All notes

HTTP Errors Need a Data Contract Too

Keep missing resources, failed dependencies, empty searches and unknown write outcomes distinct.

English edition, edited for length. Read the complete Chinese original.

A search for a nonexistent tag can legitimately return an empty list. A request for a nonexistent article that returns the homepage HTML is a routing error. Both leave the reader without the content they wanted, but their HTTP semantics and recovery actions differ. Returning 200 for both erases information that the interface, monitoring system, and crawler need to make different decisions.

This guide concerns static blogs and edge endpoints. Hosting platforms may provide a single-page application fallback that is useful for one application and wrong for another. Test the deployed hostname directly. A correct development-server response does not prove that production routes, proxies, and caches preserve the same contract.

Preserve the cause before rendering the response

The article loader needs distinguishable missing-record, index-unavailable and renderer-failed outcomes. A legitimate empty search belongs to a successful search result, not to the catch branch of a failed index read. The source’s reduced rendering branch is:

if (!article) return new Response('Article not found', { status: 404 });
try {
  return await renderArticle(article);
} catch (error) {
  logRenderFailure(error);
  return new Response('Unable to render article', { status: 500 });
}

The loader and logger are application functions. Their failure handling must not collapse upstream errors into article=null before this code sees them, and logs must exclude private bodies, credentials and unrestricted stack details from public output.

Make the recovery path depend on less

If search is unavailable, a retry button calling the same broken dependency is not the only useful exit. Offer a static article index or topic navigation when those remain independently available. Keep an error page lighter than the failing renderer. For form failures, preserve safe nonsecret inputs, put field errors near controls and expose the change to assistive technology; never repopulate passwords or one-time credentials just to preserve every field.

Verify meaning through production hosting

Request an unknown slug, a normal no-result search and controlled dependency and template failures. Inspect status, content type and body through the real proxy because SPA fallback can return homepage HTML for a missing article. Check HEAD metadata and negative-cache behavior after recovery. If privacy deliberately masks authorization as 404, retain that explicit policy internally rather than treating every denial as nonexistence. Sanitize and bound any displayed missing path so the error page cannot become an input-echo vulnerability.

HEAD and GET require different evidence

A missing article can correctly return 404 to HEAD with no body. A probe that labels every empty response a broken error template confuses method semantics with rendering failure. Use HEAD for lightweight status and metadata observations, and a separate GET when validating readable error text. Do not change a correct HEAD error into 200 merely to silence an assertion written for GET.

The converse also matters: a successful metadata-only handler does not prove that the GET rendering path can produce the body. Test both methods against existing and missing resources, and exercise an unsupported method separately so a method-contract failure is not disguised as absence. Recovery verification should then check representative content, feed and search outputs through the real domain. A disappearing alert can reflect falling traffic; it is not evidence that the user task recovered. Keep correlation identifiers useful internally without turning them into anonymous access keys for logs or request payloads.

Keep the state model intact across layers

Suppose a successful article lookup finds no matching record: the resource is missing. If the index cannot be read, the dependency failed. If the record exists but its template cannot render, execution failed. A successful search with no matches is a fourth, legitimate outcome. Preserve these distinctions from the loader through the response boundary. A single null value cannot explain all four states.

Catching an index exception and returning an empty array destroys evidence before the renderer sees it. The renderer cannot reconstruct whether there were no records or whether reading them failed. It should consume a known successful result, not infer system health from list length. Internal result branches can carry success data, confirmed absence, or a safe failure category with a correlation identifier.

The status, user message, and monitoring event can then derive from the same cause. Restricted logs may retain additional diagnostics, but those details are not part of the public contract. Do not make an alert rule parse a natural-language error title or guess from whether an HTML page was generated. A successfully rendered error page is not evidence of a successful user task.

Authorization sometimes changes what existence means publicly. A system may deliberately use 404 rather than confirm a private resource exists. That is a specific privacy policy, not permission to conceal every authorization failure arbitrarily. Internally distinguish genuine absence from denial. Decide what the requester may learn according to the threat model, and document that choice so future refactoring does not accidentally expose private existence information.

Status codes need precise causes, not approximate labels

A 404 indicates that the server did not find a current representation, or is unwilling to disclose one. A 500 describes an unexpected server condition. A gateway can use 502 for an invalid upstream response; an upstream timeout may instead call for 504. Temporary unavailability may call for 503. Do not turn every dependency exception into 502 without examining the endpoint’s role and the actual failure.

A successful search with no matches usually remains 200. Authentication and authorization also differ: 401 concerns missing or invalid authentication credentials, while 403 indicates refusal to fulfill the request. Intentional privacy masking is a separate policy. None of these distinctions requires exposing credentials, internal permission names, or stack traces to the public client.

The reduced rendering example above assumes that lookup has already succeeded and returned either a record or confirmed absence. It is not a complete server implementation. The earlier lookup still needs its own failure handling, and the logger needs a deliberate reliability and privacy policy. A thrown logging error must not accidentally cause a new unhandled failure in the error path.

Choose assertions that verify both the status and its meaning. An unknown slug should not return the title and body of the homepage. A dependency outage should not render a valid-looking empty list. A 200 response containing an error banner may still mislead automated clients unless that status is explicitly part of a partial-result contract. The number alone cannot rescue a misleading representation.

Negotiate error representations without losing the original cause

An HTML error page needs readable context and usable navigation. An API client needs a stable structure it can parse. Both can originate from the same internal category without sharing an entire representation. A JSON client receiving a branded homepage experiences a second parsing error that masks the original problem. Status, content type, and body must agree even when the request fails.

Use an established problem-details representation, such as RFC 9457, or the project’s existing schema. A problem type, status, short title, and safe diagnostic identifier can form a useful contract. Field-validation failures need stable field paths so a form can place feedback beside the controls involved. Human-readable descriptions can improve without becoming machine decision keys.

Never embed an upstream exception object wholesale because it happens to serialize. Internal paths, database statements, and credentials do not belong in public detail fields. Restricted logs can retain redacted diagnostic context. A correlation identifier should help an authorized maintainer find the event, not permit an anonymous requester to retrieve arbitrary request logs.

If the same URL supports HTML and JSON, define representation selection before reusing it for errors. Document the behavior for an unsupported media type. Cache policy must respect the chosen negotiation scheme. Test HTML and JSON requests in both orders so a cached HTML error cannot unexpectedly become the representation served to later API clients.

Preserve useful form work, not sensitive credentials

A failed form submission should not force the user to repeat every safe field. Preserve nonsecret input where appropriate, associate field errors with their controls, and distinguish those errors from a service-wide failure. An invalid email address needs different guidance from an unavailable provider. A single generic banner cannot explain both accurately or place feedback in the right location.

Do not repopulate passwords, payment information, or one-time credentials into a public response just to preserve all input. Consider where retained state lives, how long it remains, and who can read it. Useful recovery is not a reason to relax the privacy boundary. Copying the entire request into an error page can create an exposure path during an otherwise ordinary validation failure.

New feedback must be discoverable by assistive technology rather than expressed only through a changed border color. Manage focus according to the task and expose relevant status changes. Disabling submission while a request is active can reduce duplicate clicks, but restore an actionable state after a network error. Explain whether the outcome is known before offering another submission.

For a read, retry may be a simple recovery action. For a write, it may repeat a completed operation. The interface needs an unconfirmed-outcome state rather than assuming a lost response means nothing happened. Put that distinction into the shared error model before the model spreads from a blog endpoint to more consequential workflows.

Give cached absence its own lifecycle

Some caches can store responses such as 404, subject to their rules and response directives. An article address visited before release may still serve cached absence after deployment. Visibility depends on generation, upload, and cache state together. Uploading a new file is only one step in making the representation available at the public entry point.

Choose an acceptable negative-cache lifetime and understand the platform’s invalidation mechanism. If publication purges specific missing objects, its keys must match the keys used during storage. Clearing only the homepage does not remove an error cached at an article URL. Read the article through the real domain after release and verify the body, not merely a successful status from a fallback.

Do not give every non-200 response the same TTL. A confirmed missing route and a temporary dependency failure have different lifecycles. Long-lived shared caching of a transient failure can prolong an incident after recovery. Explicitly control error caching rather than assuming every provider treats the same status identically. Review platform overrides as well as application response directives.

Negative caching can reduce repeated lookups for one missing address. It offers little reuse when a scanner requests a fresh random path every time. Bound cache use, path length, and diagnostic output. Recording arbitrary missing URLs indefinitely can create its own resource problem. A cache is not a substitute for request budgets or a deliberate abuse policy.

Publishing and deletion require opposite cache transitions

Before publication, an address can resolve to absence. After publication, it should resolve to the approved article version. Registering the new addresses in the release plan makes precise invalidation and verification possible. Compare the actual response body with the intended version. Reading a generic successful page at the address does not prove that the transition completed.

Deletion moves in the opposite direction. Generating a 404 file does not necessarily remove an older cached 200 representation. Establish how stale content expires or is invalidated, and decide whether the removal needs stronger handling than ordinary freshness expiration. Test publication and deletion independently rather than treating one successful transition as proof that both work.

A temporary backend failure must not become permanent absence by accident. Keep a successful lookup that found no record distinct from a lookup that did not complete. Otherwise a brief dependency incident can populate caches with missing-resource responses at addresses that should continue to exist. Shortening the negative TTL mitigates duration but does not repair the false classification.

Observe route categories and deployed versions rather than collecting only an undifferentiated non-200 count. Rising 404 volume can reflect stale links, scanners, or a routing regression. Rising empty-search volume can indicate search-quality problems without indicating server failure. Redirects and conditional responses should not be counted as failures solely because their status is not 200.

A failed build must not look like a complete release

If a content file cannot be parsed, the usual safe policy for static publication is to fail the build and retain the last complete deployment. Silently skipping the article creates a different site while claiming success. Partial-result policies can be legitimate, but they require explicit approval and visible failure accounting. They should not emerge accidentally from a catch block.

Apply the same discipline to RSS, sitemap generation, and search indexes. A reachable feed carrying invalid XML is still broken. An unexpectedly empty feed can mislead subscribers even when the server responds normally. Validate syntax and representative entries together. Transport success does not establish that the public inventory contains the articles approved for publication.

The acceptance set should include an unknown path, a dependency timeout, invalid content, an empty search, and unavailable decorative assets. Each needs its own status, body, recovery, logging, and cache assertions. These are not interchangeable ways of exercising one generic error component. Testing only a missing slug says nothing about an exception thrown while reading the content index.

After repairing a loader, inspect the public article count and representative bodies, then verify the feed and search outputs. Alerts can stop because traffic declined or a rule was disabled. Neither proves recovery. Record the version, observation time, verified scope, and remaining uncertainties. Do not allow one passing response to stand in for every route and content channel.

Treat search failures as versioned task outcomes

Imagine a reader searching for Rust transactions when the index request times out. The query is valid, but the task did not complete. The interface must not claim there are no related articles. It can retain the query, identify any previous results as belonging to an earlier query, and offer a bounded retry plus an independently available topic index.

If the user changes the query, a late failure from the old request must not replace the new state. Associate results and errors with a request generation or equivalent identity. An error belongs to a particular attempt, not to a permanent global string on the page. A retry should use the current intended parameters rather than silently resurrect an obsolete query.

The server supplies a stable safe category; the client chooses an appropriate recovery action. Temporary unavailability may permit bounded read retries, invalid input requires correction, and an access problem may require authentication or a route to available content. Keep user-facing wording independent of machine branching. Translating or improving a message should not change which recovery operation runs.

Monitoring should classify the example as a dependency timeout rather than an empty search. Rendering an error message proves only that the failure experience worked. Track successful tasks, empty results, failures, and recovery separately with clear denominators. A synthetic experiment establishes behavior; it cannot establish a real production failure percentage or justify an invented improvement claim.

A write timeout needs an unknown-result branch

When middleware is reused for saving articles, a network timeout does not establish that the save failed. The database may have committed before the response was lost. Retain an unconfirmed state and query the current version or operation identity before permitting an unconditional repeat. Transport uncertainty and business failure must not be collapsed into the same message.

A notification can fail after the primary write succeeded. Catching the whole workflow and returning one undifferentiated 500 can cause a client to repeat an already completed operation. Define the transaction’s success boundary separately from subsequent delivery obligations. Where appropriate, a reliable background task handles delivery without pretending the primary write never happened.

Distinguish not executed, executed with a later failure, and unable to confirm. A thrown exception alone does not prove there was no side effect. Test failure before commit, response loss after commit, and notification failure separately using controlled counters. Check that the client does not mark all three cases as safe to repeat. Keep fault injection away from real production writes.

Idempotency also requires a contract: scope, lifetime, binding to request content, and behavior on key reuse. Adding a UUID field alone does not solve duplication. The interface should help establish the real outcome rather than making repeated clicks the only diagnostic mechanism. Preserving the user’s input is helpful only if resubmission semantics remain truthful.

Fault injection should prove the deployed recovery boundary

In preview or controlled tests, make the index reader throw a known exception. Separately request a missing slug, fail the renderer, and perform a successful no-match search. These must not collapse into the same successful empty page. Check safe correlation identifiers without exposing stacks. Keep diagnostic access separate from possession of a public identifier.

A host may rewrite unknown paths to the homepage even when the project contains a 404 component. Request a random deep URL directly rather than navigating only through known links. Inspect the deployed status and body. If fallback routing is responsible, repair that layer: changing the visual design of a 404 page will not make the host choose it.

A proxy may replace a structured API failure with branded HTML. Pass a fixed error representation through the real proxy and compare status, content type, and safe fields. Repair the substitution where it occurs, rather than teaching each client to guess the cause from an HTML title after JSON parsing has failed. Include both application-generated and gateway-generated failures in the test plan.

Do not manufacture real database outages in production. Use controlled samples and restrict any test-only endpoints; avoid anonymous triggers that reveal configuration or logs. After recovery, confirm cached failures are gone, safe form work can resume, and alerts recover for the right reason. The error contract ends with truthful failure handling and evidence of recovery, not merely an exception recorded somewhere.

Keep a small acceptance matrix beside the implementation

Write down the expected outcome for each controlled case before running the checks. An absent article needs the missing-resource response; an unreadable index needs a dependency-failure response; a renderer exception needs an execution-failure response; a valid no-match search needs a successful empty result. For each row, record the request method, representation, cache expectation, and an independently usable recovery action. This makes disagreement visible without relying on an engineer remembering the intended behavior during an incident.

Repeat representative rows through the production hostname after an approved deployment, without inducing real production faults. Preview fault injection and production content verification provide different evidence. Keep both, and make untested boundaries explicit rather than treating one green status indicator as proof of the entire contract.

References