writeInternalError logs the real error server-side and sends a generic message to the client. This prevents leaking SQL errors, file paths, and other internal details.
(w http.ResponseWriter, err error)
| 2247 | // message to the client. This prevents leaking SQL errors, file paths, |
| 2248 | // and other internal details. |
| 2249 | func writeInternalError(w http.ResponseWriter, err error) { |
| 2250 | // The store's NUL refusal is a DECISION, not a fault, and it is mapped |
| 2251 | // here rather than at each handler for the same reason Layer A lives at |
| 2252 | // the driver: this is the one funnel every 500 already passes through, so |
| 2253 | // mapping it once covers every handler that exists and every one that will |
| 2254 | // (DOC-2823 S1). Enumerating error blocks is what the item-title unit had |
| 2255 | // to do three times in one function before a structural test caught the |
| 2256 | // third. |
| 2257 | // |
| 2258 | // 400, matching what the HTTP gate answers for the SAME value refused at |
| 2259 | // the door. Two statuses for one rule would be the layers disagreeing |
| 2260 | // again, in the response this time instead of in the predicate. |
| 2261 | // |
| 2262 | // The honest residual: a value can reach the store from something the |
| 2263 | // SERVER composed rather than the caller supplied — that is BUG-2814's |
| 2264 | // re-emit population — and for those a 400 tells the caller their request |
| 2265 | // was bad when it was our stored data. It is still the better answer than |
| 2266 | // 500, because the request is understood and will be refused identically |
| 2267 | // on retry, and the log line below keeps the detail. If the re-emit case |
| 2268 | // ever needs its own status, it needs its own error type first. |
| 2269 | if reason, ok := nulRefusalReason(err); ok { |
| 2270 | slog.Warn("write refused: invalid text parameter", "error", err) |
| 2271 | writeError(w, http.StatusBadRequest, "bad_request", reason) |
| 2272 | return |
| 2273 | } |
| 2274 | // The outbox row cap is likewise a decision, not a fault, and is mapped in |
| 2275 | // the same funnel for the same reason (BUG-2827). 413 with a *_too_large |
| 2276 | // code follows the house precedent set by rename_cascade_too_large rather |
| 2277 | // than inventing a second spelling for "this operation produces more than |
| 2278 | // the server will process in one go". |
| 2279 | // |
| 2280 | // The literal reading of 413 is that the REQUEST entity is too large, and |
| 2281 | // here the request is small while the event it derives is not. Taken |
| 2282 | // alone that argues for 422. It loses to consistency: an operator watching |
| 2283 | // for cascade refusals should not have to know which of two size bounds |
| 2284 | // fired to know which status to grep for, and the message says plainly |
| 2285 | // what was actually too big. |
| 2286 | // |
| 2287 | // Composed from the TYPED fields, never by splicing err.Error(), so no |
| 2288 | // wrapper the call path added is published to the caller. |
| 2289 | var oversized *store.OversizedOutboxPayloadError |
| 2290 | if errors.As(err, &oversized) { |
| 2291 | slog.Warn("write refused: outbox payload over the size limit", |
| 2292 | "event_type", oversized.EventType, "bytes", oversized.Bytes, "limit", oversized.Limit) |
| 2293 | // The message names WHAT was measured, because the store refuses on two |
| 2294 | // different measurements — the member content before marshalling, and |
| 2295 | // the row as stored — and a caller told only "%d bytes" for both |
| 2296 | // cannot reconcile two different numbers for one mutation (codex |
| 2297 | // round 3). |
| 2298 | writeError(w, http.StatusRequestEntityTooLarge, "event_payload_too_large", |
| 2299 | fmt.Sprintf("This change would record a %s event larger than the server will store in one "+ |
| 2300 | "row: %s is %d bytes, and the limit is %d. Split the change into smaller ones and try again.", |
| 2301 | oversized.EventType, oversized.Measured, oversized.Bytes, oversized.Limit)) |
| 2302 | return |
| 2303 | } |
| 2304 | slog.Error("internal server error", "error", err) |
| 2305 | writeError(w, http.StatusInternalServerError, "internal_error", "An internal error occurred") |
| 2306 | } |