Map the Sume error envelope to RFC 9457 problem details in 15 lines

RFC 9457 defines type, status, title, detail and instance. Sume's error.code, message and request_id map onto them; keep the retry fields as extensions.

4 min readSume
All posts

If your gateway or your clients speak RFC 9457 problem details, you can translate a Sume error in a few lines. error.code becomes the problem type, message becomes detail, the HTTP status becomes status, request_id becomes instance, and the retry fields ride along as extension members. Sume's own body is a JSON {"error": {...}} envelope, and this post does not claim that Sume sends application/problem+json itself.

What the RFC defines

RFC 9457 names the media type application/problem+json and five standard members: type (a URI reference that identifies the problem type), status (the HTTP status code generated by the origin server), title (a short summary of the problem type), detail (an explanation specific to this occurrence) and instance (a URI reference that identifies the specific occurrence). Problem types may add extension members, and status, if present, is only advisory.

The mapping

Every Sume field fits somewhere, and the actionability fields fit best as extensions, because they are specific to this API:

Sume error envelope fields mapped to RFC 9457 members (Sume docs and RFC 9457, read 2026-10-05)
Sume fieldRFC 9457 memberNote
error.codetypeWrap as a URI, for example urn:sume:error:<code>
HTTP statusstatusAdvisory per the RFC; keep the real response status
error.public_reasontitleA short stable label for the problem type
error.messagedetailSpecific to this occurrence
error.request_idinstanceWrap as a URI, for example urn:sume:request:<id>
category, stage, retryable, retry_after_seconds, next_action, detailsextensionsKeep the names unchanged

The converter

This runs as is, and it keeps unknown fields out, so you never leak more than the envelope carried:

import json

EXT = ("category", "stage", "retryable", "retry_after_seconds", "next_action", "details")

def to_problem(status: int, body: dict) -> dict:
    e = body["error"]
    p = {
        "type": f"urn:sume:error:{e['code']}",
        "status": status,
        "title": e.get("public_reason") or e["code"],
        "detail": e.get("message", ""),
    }
    if e.get("request_id"):
        p["instance"] = f"urn:sume:request:{e['request_id']}"
    p.update({k: e[k] for k in EXT if k in e})
    return p

body = {"error": {"code": "queue_full", "message": "Queue is full.", "request_id": "req_1", "retryable": True, "retry_after_seconds": 30}}
print(json.dumps(to_problem(429, body)))

Why bother

A shared problem shape lets one error middleware serve many upstreams. Your logs, alerts and front end can read type and instance for every vendor, and read the Sume extensions only when type begins with your Sume prefix. The cost is small: fifteen lines and a table. Keep the original request_id as the instance, because it is the string support will ask for first.

Limits

The type URIs above are yours, not Sume's, and they do not have to resolve. Choose a namespace you control if you publish them. The authoritative field list is on the Errors and credits page, and the RFC is at rfc-editor.org.

Keep the converter next to your HTTP client, not inside each call site, and cover it with a table test over the codes you handle. Because retryable and next_action are copied unchanged, your retry logic can read the same names whether the error arrived raw or as problem details.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume