Module: Wurk::API::Problem
- Defined in:
- lib/wurk/api/problem.rb
Overview
Error bodies for the HTTP API, shaped like RFC 9457 problem documents.
One deliberate divergence from the RFC: type is a bare stable slug
('not_found'), not a URI. A URI would either hardcode a docs host that can
move or emit a mount-relative path the client can't dereference, and the
slug is the part a client actually branches on. /v1 makes these slugs a
contract: adding one is fine, renaming one is a breaking change.
Constant Summary collapse
- CONTENT_TYPE =
'application/problem+json'- NOT_FOUND =
'not_found'- METHOD_NOT_ALLOWED =
'method_not_allowed'- UNSUPPORTED_API_VERSION =
'unsupported_api_version'- INTERNAL_ERROR =
'internal_error'- UNAUTHORIZED =
'unauthorized'- INVALID_REQUEST =
'invalid_request'- JOB_NOT_FOUND =
Distinct from
not_found, which means the API has no such route. A client that asked to cancel a job needs to tell "you addressed nothing" apart from "that job already ran". 'job_not_found'- BATCH_NOT_FOUND =
The same distinction one addressable resource over: a well-formed bid the
batchesset has never held, as opposed to a mistyped path. 'batch_not_found'- FLOW_NOT_FOUND =
And one relation further out: a well-formed fid the
flowsset has never held. Distinct frombatch_not_foundbecause a flow's nodes are batches — a client told "batch not found" for a fid would go looking for the wrong thing. 'flow_not_found'- PROCESS_NOT_FOUND =
A well-formed identity that no live heartbeat answers to — the process exited, or its beat lapsed and Redis reaped the row.
'process_not_found'- PROCESS_NOT_SIGNALABLE =
The process is live and the caller may signal it, but this one cannot be signalled at all: an embedded process shares its host application's PID, so a TSTP or TERM aimed at it would hit the web server around it.
'process_not_signalable'- INSUFFICIENT_SCOPE =
Named for RFC 6750 §3.1 so the slug and the
error=the 403 carries in WWW-Authenticate are the same word. 'insufficient_scope'- PAYLOAD_TOO_LARGE =
Separate from
invalid_requestbecause the client's fix is different: nothing about the request's shape is wrong, it is only too big, and themax_bytesextension member says by how much. 'payload_too_large'- CLASS_NOT_ALLOWED =
An authorization verdict, like
insufficient_scope— the credential is valid and the body is well-formed, but this class is not one the HTTP API may enqueue. 'class_not_allowed'- IDEMPOTENCY_KEY_REUSED =
The two ways an
Idempotency-Keycollides. Reused means the same key arrived with different bytes, which is a client bug it must fix by rotating the key; in-progress means the first request holding it has not answered yet, which it fixes by waiting. 'idempotency_key_reused'- REQUEST_IN_PROGRESS =
'request_in_progress'- READ_ONLY =
Not
insufficient_scope: the credential is granted everything it needs and would work against another deployment unchanged. Nothing the client can send fixes this one, which is why it gets its own slug — a producer that retried a 403 forever on a frozen deployment is the failure this avoids. 'read_only'- RATE_LIMITED =
The one refusal that comes with a time attached.
retry_afterrepeats the header as a number so a client that already parses this body does not have to reach back into the headers for it. 'rate_limited'- TITLES =
Every slug needs a human title;
fetchbelow turns a missing one into a loud failure in the test suite rather than a half-formed error body. { NOT_FOUND => 'Not Found', METHOD_NOT_ALLOWED => 'Method Not Allowed', UNSUPPORTED_API_VERSION => 'Unsupported API Version', INTERNAL_ERROR => 'Internal Server Error', UNAUTHORIZED => 'Unauthorized', INSUFFICIENT_SCOPE => 'Insufficient Scope', INVALID_REQUEST => 'Invalid Request', JOB_NOT_FOUND => 'Job Not Found', BATCH_NOT_FOUND => 'Batch Not Found', FLOW_NOT_FOUND => 'Flow Not Found', PROCESS_NOT_FOUND => 'Process Not Found', PROCESS_NOT_SIGNALABLE => 'Process Not Signalable', PAYLOAD_TOO_LARGE => 'Payload Too Large', CLASS_NOT_ALLOWED => 'Class Not Allowed', IDEMPOTENCY_KEY_REUSED => 'Idempotency Key Reused', REQUEST_IN_PROGRESS => 'Request In Progress', READ_ONLY => 'Read-Only Mode', RATE_LIMITED => 'Too Many Requests' }.freeze
Class Method Summary collapse
-
.from(error, instance:) ⇒ Object
Renders a refusal that already knows which problem it is — the shape Validation::Invalid carries.
-
.render(type, status:, detail:, instance:, headers: nil, **extra) ⇒ Object
extrakeywords become extension members (RFC 9457 §3.2), e.g. -
.response_headers(extra) ⇒ Object
nosniff so a browser pointed at an error can never be talked into rendering the reflected request path as anything but data.
Class Method Details
.from(error, instance:) ⇒ Object
Renders a refusal that already knows which problem it is — the shape Validation::Invalid carries. The mapping from a rejection to a status code stays with the check that made it, so a new rejection never needs a new arm in the route that surfaces it.
109 110 111 |
# File 'lib/wurk/api/problem.rb', line 109 def from(error, instance:) render(error.type, status: error.status, detail: error., instance: instance, **error.extra) end |
.render(type, status:, detail:, instance:, headers: nil, **extra) ⇒ Object
extra keywords become extension members (RFC 9457 §3.2), e.g.
supported_versions:. Returns a Rack response triple.
97 98 99 100 101 102 103 |
# File 'lib/wurk/api/problem.rb', line 97 def render(type, status:, detail:, instance:, headers: nil, **extra) body = { type: type, title: TITLES.fetch(type), status: status, detail: detail, instance: instance } body.merge!(extra) [status, response_headers(headers), [::JSON.generate(body)]] end |
.response_headers(extra) ⇒ Object
nosniff so a browser pointed at an error can never be talked into rendering the reflected request path as anything but data.
115 116 117 118 |
# File 'lib/wurk/api/problem.rb', line 115 def response_headers(extra) headers = { 'content-type' => CONTENT_TYPE, 'x-content-type-options' => 'nosniff' } extra ? headers.merge(extra) : headers end |