Payments are experiencing issues due to temporary restrictions in Russia. If your payment does not go through, please submit a support request.Our support team is available 24/7 — we are always here to help with hosting and server issues.We are now accepting requests for dedicated server rental and colocation services in our data center.Reminder: we recommend enabling backups for additional data protection.A new VPS/VDS lineup with NVMe storage and improved performance is now available.Maintenance work on some servers has been completed. All services are operating normally.
Article8 min readViews0

application/problem+json: How Not to Mistake an HTML Error for JSON

A practical guide to RFC 9457: first check the HTTP status and Content-Type, then safely parse Problem Details. Using Python 3.12, we reproduce a correct 422 response, HTML from a gateway, broken JSON, incorrect types, and status mismatches.

Comments 0

A package on a separate control conveyor next to an amber signal lamp and a scanner
In this article

An API client must not begin by blindly parsing JSON. First, it checks the actual HTTP status and the basic media type of the response. Only application/problem+json is passed to the Problem Details handler; HTML from a proxy, an empty body, and broken JSON are treated as distinct errors. The status field inside the body is useful for logging but does not replace the HTTP status line.

In a small example, an import operation receives five responses: a correct 422, HTML from a gateway with 502, corrupted JSON, fields with incorrect types, and a mismatch between HTTP 503 and status 429 in the body. A single handler must distinguish these cases without exposing stack traces to the operator or accepting an external format as an agreement.

What Problem Details Provides

RFC 9457 describes the Problem Details object and the application/problem+json media type. The format is intended for machine-readable error details and does not override HTTP status semantics. The specification also warns that problem details should not become a debugging dump: internal information may reveal system internals. A detailed description of the fields is provided in RFC 9457.

  • type — the primary identifier for the problem type; defaults to about:blank if absent
  • title — a short, human-readable name for the problem type
  • status — a copy of the HTTP status for convenience, not a new source of truth
  • detail — an explanation specific to this case
  • instance — an identifier for the specific occurrence of the problem

Extensions are allowed. A client unaware of them must ignore them, not reject the entire response. Therefore, the sample trace_id does not appear in the normalized result: the general handler does not need it.

The first filter is Content-Type

The charset parameter is allowed, so we compare only the base media type, ignoring case and whitespace. We do not pass a text/html response with status 502 to json.loads: otherwise, a genuine gateway failure would turn into a misleading JSONDecodeError and lose the original status code.

HTTP and Content-Type

Client action

What to save

422; application/problem+json

Parse the object

HTTP 422, type, title, detail, instance

502; text/html

Do not parse as JSON

HTTP 502 and a safe reason for format rejection

500; application/problem+json, corrupted body

Stop parsing

HTTP 500 and the invalid problem JSON flag

503; status 429 in the body

Flag discrepancy

HTTP 503, body status 429, mismatch

Minimal testable client

The check was performed on October 18, 2026, on CPython 3.12.14 using only the standard library. This is a pure response model: the external network, a live store, and the 1C-Bitrix API are not used.

from dataclasses import dataclass import json @dataclass(frozen=True) class Response: status: int content_type: str body: bytes def parse_problem(response: Response) -> dict: media_type = response.content_type.split(";", 1)[0].strip().lower() if media_type != "application/problem+json": raise ValueError(f"unexpected media type: {media_type or 'missing'}") try: raw = json.loads(response.body) except (UnicodeDecodeError, json.JSONDecodeError) as error: raise ValueError("invalid problem JSON") from error if not isinstance(raw, dict): raise ValueError("problem details must be a JSON object") def text(name: str, default: str | None = None) -> str | None: value = raw.get(name, default) return value if isinstance(value, str) else default body_status = raw.get("status") if not isinstance(body_status, int) or isinstance(body_status, bool): body_status = None return { "http_status": response.status, "type": text("type", "about:blank"), "title": text("title"), "detail": text("detail"), "instance": text("instance"), "body_status": body_status, "status_mismatch": body_status is not None and body_status != response.status, }

The function accepts an already received response. It does not attempt to automatically open the type URI, does not output raw body content to the user, and discards standard fields of the wrong type. A boolean value for status is also not accepted: in Python, bool is a subclass of int, so this condition is checked separately.

Five responses and the expected result

cases = { "valid": Response( 422, "application/problem+json; charset=utf-8", json.dumps({ "type": "https://api.example.test/problems/invalid-stock", "title": "Invalid stock", "status": 422, "detail": "stock must be zero or greater", "instance": "/imports/7/items/42", "trace_id": "demo-123", }).encode(), ), "html_gateway": Response(502, "text/html", b"<h1>Bad Gateway</h1>"), "broken_json": Response(500, "application/problem+json", b'{"title":'), "wrong_types": Response( 400, "application/problem+json", b'{"type":7,"title":"Bad request","status":"400"}', ), "mismatch": Response( 503, "application/problem+json", b'{"title":"Busy","status":429}', ), } for name, response in cases.items(): try: problem = parse_problem(response) except ValueError as error: print(f"{name}: reject: {error}") else: print( f"{name}: accept: http={problem['http_status']} " f"type={problem['type']} body_status={problem['body_status']} " f"mismatch={problem['status_mismatch']}" )

valid: accept: http=422 type=https://api.example.test/problems/invalid-stock body_status=422 mismatch=False html_gateway: reject: unexpected media type: text/html broken_json: reject: invalid problem JSON wrong_types: accept: http=400 type=about:blank body_status=None mismatch=False mismatch: accept: http=503 type=about:blank body_status=429 mismatch=True

The script exited with code 0. The correct 422 was accepted; HTML and corrupted JSON were rejected before business processing; incorrect types did not corrupt the entire object; the 503/429 mismatch became an explicit flag. These are observations from a sample run, not a measurement of a specific external service.

Three paths for API response handling: Problem Details are parsed, HTML is rejected, and status mismatches are flagged.
Five-test scheme: response format is validated before JSON, while HTTP status is kept separate from the status field in the body.

Why the status in the body must not control retries

RFC refers to status in the object as advisory. An intermediate node may change the HTTP status while the saved body remains unchanged. If the network reports 503 but the JSON claims 429, the client must not silently choose the convenient number. It preserves both values, flags the discrepancy, and applies policy based on the actual HTTP response and the integration contract.

A pause after 429 requires separate parsing of the Retry-After header. It is verified in the article 429 Retry-After: seconds or date. Problem Details may explain the error but does not replace retry rules.

How to embed this into a store integration

For 1C-Bitrix: Site Management is not a promise of a built-in format for a specific method. First, the contract must explicitly specify the external API, version, operation, permissions, allowed HTTP statuses, and Content-Type. If the provider returns its own error schema, a separate adapter is required; renaming fields to Problem Details on the client side is insufficient.

  • The transport layer preserves the HTTP status, Content-Type, and a limited diagnostic identifier without secrets.
  • The dispatcher selects the handler based on the base media type.
  • The Problem Details handler validates the JSON object and the types of standard fields.
  • The business layer makes a decision based on the HTTP status, type, and documented extensions.
  • An unknown type or extension does not trigger guesses: the package is either stopped or sent for manual review.
  • The raw body and instance are not displayed to the buyer without checking for personal and internal data.

Acceptance Testing

  • Submit a valid application/problem+json with the charset parameter; expect the preserved HTTP status, type, and detail.
  • Submit text/html with 502; ensure the JSON parser is not invoked and the original HTTP status is not lost.
  • Submit corrupted JSON with the correct media type; expect a separate error for invalid problem JSON.
  • Submit an array instead of an object and standard fields of incorrect types; reject the array and ignore incorrect fields per the contract.
  • Submit a mismatched HTTP status and status field; preserve both values and raise a mismatch.
  • Add an unknown extension; ensure basic fields continue to be processed.
  • Check the log: it must not contain tokens, cookies, full HTML, personal data, or stack traces.

Example Boundaries

The test code does not perform an HTTP request, does not check TLS, body size, Content-Encoding, or memory limits. In a production client, these are checked before parsing. A limit is also needed for detail and logging; otherwise, even a correct media type could still deliver text that is too large or sensitive.

Problem Details helps unify the error format, but it does not fix an incorrect status, does not define a retry strategy, and does not replace documentation for the problem type. The readiness criterion is simple: every unexpected format or status conflict remains visible instead of turning into a random JSON parser exception.

Discussion 0

Share your experience and ask questions. Comments without links appear after editorial review.

No comments yet. Start the discussion.