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 batches set 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 flows set has never held. Distinct from batch_not_found because 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_request because the client's fix is different: nothing about the request's shape is wrong, it is only too big, and the max_bytes extension 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-Key collides. 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_after repeats 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; fetch below 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

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.message, 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