Skip to content

fix: proper 404s + JSON profile pages - #60

Open
jb55 wants to merge 3 commits into
masterfrom
fix-404-and-json-profiles
Open

fix: proper 404s + JSON profile pages#60
jb55 wants to merge 3 commits into
masterfrom
fix-404-and-json-profiles

Conversation

@jb55

@jb55 jb55 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes spurious 503s on missing notes and rounds out the JSON API.

1. Return proper HTTP status codes instead of aborting the connection

When serve() returned Err (e.g. Error::NotFound for a missing note), hyper's serve_connection aborted the connection without writing any HTTP response. A reverse proxy in front then turned the dropped upstream connection into a 502/503, so a routine "note not found" surfaced to clients as a 503 instead of a 404. The service closure now catches the error and maps it to a real response: NotFound → 404, everything else → 500. 4xx log at debug (unpropagated notes / bad ids are routine); 5xx still log at error.

2. Styled 404 page

A shared, Damus-themed 404 page (not_found_response) replaces the bare "Profile not found :(" text and plain-text "Invalid url". Used by the missing-note/profile, invalid-bech32, and profile-missing paths.

3. JSON profile pages + JSON 404s

serve_profile_json (previously a "todo: profile json" stub) returns the parsed profile metadata from nostrdb — pubkey, name, display_name, about, picture, banner, website, nip05, lud06, lud16, lnurl (empty fields omitted) — plus a recent_notes array (the same 12-note newest-first feed the HTML page shows). The .json endpoints now return JSON-shaped 404s ({"error":"not_found","message":...}) instead of the HTML 404 page.

Testing

  • cargo test — all 34 pass
  • Runtime: missing/invalid note .json → 404 application/json; real profile .json → broken-out metadata + 12 recent notes; missing note HTML → styled 404; homepage → 200

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Profile endpoint now supports JSON format for structured data access.
  • Improvements

    • Implemented improved error pages with descriptive messages when content is not found.
    • Enhanced request handling for JSON-formatted responses via .json suffix.
    • Better error messaging for invalid request paths.

jb55 and others added 3 commits June 7, 2026 09:13
When serve() returned Err (e.g. Error::NotFound for a missing note),
hyper's serve_connection aborted the connection without writing any
HTTP response. A reverse proxy in front then translated the dropped
upstream connection into a 502/503, so a routine "note not found"
surfaced to clients as a 503 instead of a 404.

Catch the error in the service closure and map it to a real response:
NotFound -> 404, everything else -> 500. 4xx are logged at debug since
unpropagated notes and bad ids are routine; 5xx still log at error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the bare "Profile not found :(" text and plain-text "Invalid url"
response with a shared Damus-styled 404 page (render_not_found_html /
not_found_response). The main error handler now serves this page for
Error::NotFound, and the nip19-parse-failure and profile-missing paths
reuse it too, so every 404 gets consistent branding and a link home.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement serve_profile_json (previously a "todo: profile json" stub).
It returns the profile metadata broken out from nostrdb's parsed profile
record — pubkey, name, display_name, about, picture, banner, website,
nip05, lud06, lud16, lnurl (empty fields omitted) — plus a recent_notes
array (the same 12-note newest-first feed the HTML profile page shows).

Also serve JSON-shaped 404s on the .json endpoints instead of the HTML
404 page: not_found_json returns {"error":"not_found","message":...}.
The main error handler picks JSON vs HTML based on the request path, and
the invalid-bech32 path in serve() does the same. Drop the last inline
"Profile not found :(" in favor of the shared profile_not_found helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a JSON-serialized profile endpoint with refactored 404 response helpers. New serve_profile_json returns profile metadata and recent notes as JSON. Shared 404 helpers now support both HTML and JSON error responses. The request handler detects JSON requests by .json suffix and routes errors accordingly.

Changes

Profile JSON Endpoint and 404 Improvements

Layer / File(s) Summary
404 Response Helpers
src/html.rs
Introduces render_not_found_html(message), not_found_response(message), and not_found_json(message) for styled error pages and JSON responses. Refactors profile_not_found to return a concrete Response<Full<Bytes>> instead of Result, and updates serve_profile_html to use the refactored helper for missing profile early returns.
Profile JSON Endpoint
src/html.rs
New serve_profile_json function that extracts parsed profile metadata (pubkey, name, display_name, about, picture, banner, website, nip05, lud06, lud16, lnurl) into a serde_json::Map and populates a recent_notes array from kind-1 notes queried via NDB, returning Error::NotFound when the profile is missing.
Request Handler Integration
src/main.rs
Adds debug to tracing imports. Routes invalid NIP-19 paths to not_found_response or not_found_json based on JSON detection. Replaces Profile JSON rendering placeholder with a call to html::serve_profile_json. Refactors the Hyper connection loop to detect JSON requests by .json suffix, convert Error::NotFound to appropriate not-found HTML/JSON responses (with debug logging), and convert other errors to HTTP 500 responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A profile in JSON, styled 404s so fine,
From HTML helpers now shared—quite divine!
Debug logs whisper secrets as errors align,
Recent notes hop along in format's design,
The endpoint now speaks both tongues, side by side! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'fix: proper 404s + JSON profile pages' clearly and concisely summarizes the main changes: implementing proper 404 error handling and adding JSON endpoints for profile pages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-404-and-json-profiles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/html.rs`:
- Around line 415-423: The current code swallows errors from ndb.query(...)
causing empty feeds instead of surfacing backend failures; change the block that
starts with ndb.query(...) to propagate query errors (use the ? operator or
return Err(...) from the enclosing function when ndb.query fails) rather than
using if let Ok(...). Keep the existing sorting and JSON parsing loop
(serde_json::from_str, result.note.json()) but ensure the call to
ndb.query(&txn, &[notes_filter], PROFILE_FEED_RECENT_LIMIT as i32) is assigned
with a fallible binding (e.g., let results = ndb.query(...) ?;) so query
failures are returned to the caller.

In `@src/main.rs`:
- Around line 726-731: In the generic error arm (`_ => { ... }`) where `err` is
currently interpolated into the response body, keep logging the full `err`
server-side but stop returning it to clients; instead detect if the request
expects JSON (inspect the request's Accept header or the request path/extension)
and, for JSON requests, return a 500 with a generic JSON body (e.g.
{"error":"internal_server_error"}) and Content-Type application/json, otherwise
return a generic plain-text 500 message; update the `Response::builder()` call
that currently uses `Bytes::from(format!("{}\n", err))` to produce the sanitized
payload while leaving the `error!` log unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f611cea-50ab-4fa2-922b-920bc98e0733

📥 Commits

Reviewing files that changed from the base of the PR and between c286453 and eb121de.

📒 Files selected for processing (2)
  • src/html.rs
  • src/main.rs

Comment thread src/html.rs
Comment on lines +415 to +423
if let Ok(mut results) = ndb.query(&txn, &[notes_filter], PROFILE_FEED_RECENT_LIMIT as i32) {
results.sort_by_key(|result| result.note.created_at());
results.reverse();
for result in &results {
if let Ok(note_value) = serde_json::from_str::<Value>(&result.note.json()?) {
recent_notes.push(note_value);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate recent-note query failures instead of returning an empty feed.

Lines 415-423 swallow ndb.query(...) errors and still return 200 with recent_notes: []. That makes a backend failure indistinguishable from a genuinely empty profile, so API clients can cache incomplete data as truth.

Suggested fix
-    if let Ok(mut results) = ndb.query(&txn, &[notes_filter], PROFILE_FEED_RECENT_LIMIT as i32) {
-        results.sort_by_key(|result| result.note.created_at());
-        results.reverse();
-        for result in &results {
-            if let Ok(note_value) = serde_json::from_str::<Value>(&result.note.json()?) {
-                recent_notes.push(note_value);
-            }
-        }
-    }
+    let mut results = ndb.query(&txn, &[notes_filter], PROFILE_FEED_RECENT_LIMIT as i32)?;
+    results.sort_by_key(|result| result.note.created_at());
+    results.reverse();
+    for result in &results {
+        if let Ok(note_value) = serde_json::from_str::<Value>(&result.note.json()?) {
+            recent_notes.push(note_value);
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Ok(mut results) = ndb.query(&txn, &[notes_filter], PROFILE_FEED_RECENT_LIMIT as i32) {
results.sort_by_key(|result| result.note.created_at());
results.reverse();
for result in &results {
if let Ok(note_value) = serde_json::from_str::<Value>(&result.note.json()?) {
recent_notes.push(note_value);
}
}
}
let mut results = ndb.query(&txn, &[notes_filter], PROFILE_FEED_RECENT_LIMIT as i32)?;
results.sort_by_key(|result| result.note.created_at());
results.reverse();
for result in &results {
if let Ok(note_value) = serde_json::from_str::<Value>(&result.note.json()?) {
recent_notes.push(note_value);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/html.rs` around lines 415 - 423, The current code swallows errors from
ndb.query(...) causing empty feeds instead of surfacing backend failures; change
the block that starts with ndb.query(...) to propagate query errors (use the ?
operator or return Err(...) from the enclosing function when ndb.query fails)
rather than using if let Ok(...). Keep the existing sorting and JSON parsing
loop (serde_json::from_str, result.note.json()) but ensure the call to
ndb.query(&txn, &[notes_filter], PROFILE_FEED_RECENT_LIMIT as i32) is assigned
with a fallible binding (e.g., let results = ndb.query(...) ?;) so query
failures are returned to the caller.

Comment thread src/main.rs
Comment on lines +726 to +731
_ => {
error!("serve error (500): {}", err);
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from(format!("{}\n", err))))
.expect("building error response")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't expose raw internal errors on .json requests.

Lines 726-731 return format!("{}\n", err) for every 500. That leaks internal error details to clients, and .json endpoints stop being JSON on the error path. Log the real error server-side and return a generic structured 500 instead.

Suggested fix
-                                        _ => {
-                                            error!("serve error (500): {}", err);
-                                            Response::builder()
-                                                .status(StatusCode::INTERNAL_SERVER_ERROR)
-                                                .body(Full::new(Bytes::from(format!("{}\n", err))))
-                                                .expect("building error response")
-                                        }
+                                        _ => {
+                                            error!("serve error (500): {}", err);
+                                            if wants_json {
+                                                Response::builder()
+                                                    .status(StatusCode::INTERNAL_SERVER_ERROR)
+                                                    .header(
+                                                        header::CONTENT_TYPE,
+                                                        "application/json; charset=utf-8",
+                                                    )
+                                                    .body(Full::new(Bytes::from(
+                                                        r#"{"error":"internal_server_error","message":"Internal server error"}"#,
+                                                    )))
+                                                    .expect("building error response")
+                                            } else {
+                                                Response::builder()
+                                                    .status(StatusCode::INTERNAL_SERVER_ERROR)
+                                                    .header(
+                                                        header::CONTENT_TYPE,
+                                                        "text/plain; charset=utf-8",
+                                                    )
+                                                    .body(Full::new(Bytes::from(
+                                                        "Internal server error\n",
+                                                    )))
+                                                    .expect("building error response")
+                                            }
+                                        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_ => {
error!("serve error (500): {}", err);
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from(format!("{}\n", err))))
.expect("building error response")
_ => {
error!("serve error (500): {}", err);
if wants_json {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(
header::CONTENT_TYPE,
"application/json; charset=utf-8",
)
.body(Full::new(Bytes::from(
r#"{"error":"internal_server_error","message":"Internal server error"}"#,
)))
.expect("building error response")
} else {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(
header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)
.body(Full::new(Bytes::from(
"Internal server error\n",
)))
.expect("building error response")
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 726 - 731, In the generic error arm (`_ => { ...
}`) where `err` is currently interpolated into the response body, keep logging
the full `err` server-side but stop returning it to clients; instead detect if
the request expects JSON (inspect the request's Accept header or the request
path/extension) and, for JSON requests, return a 500 with a generic JSON body
(e.g. {"error":"internal_server_error"}) and Content-Type application/json,
otherwise return a generic plain-text 500 message; update the
`Response::builder()` call that currently uses `Bytes::from(format!("{}\n",
err))` to produce the sanitized payload while leaving the `error!` log
unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant