Skip to content

fix(frontend): return the Anthropic error envelope on bad request bodies - #13698

Open
ayaangazali wants to merge 2 commits into
ai-dynamo:mainfrom
ayaangazali:fix/anthropic-json-error-envelope
Open

ayaangazali wants to merge 2 commits into
ai-dynamo:mainfrom
ayaangazali:fix/anthropic-json-error-envelope

Conversation

@ayaangazali

@ayaangazali ayaangazali commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

/v1/messages and /v1/messages/count_tokens answer every failure they produce themselves with the nested Anthropic envelope, and #13102 added the same envelope for unmatched Anthropic routes, with the note that Anthropic clients expect that shape:

{"type":"error","error":{"type":"not_found_error","message":"Route not found: GET /v1/messages/missing"}}

Requests that fail before reaching the handler did not get it. Both handlers used Axum's Json extractor, whose rejections are text/plain. Measured on main with the Anthropic endpoints enabled:

Content-Type: text/plain  ->  415  text/plain  Expected request with `Content-Type: application/json`
body "{not json"          ->  400  text/plain  Failed to parse the request body as JSON: ...

So a client written against the documented envelope parses an unmatched route fine and throws on the two most common request mistakes.

I checked the obvious counter-hypothesis before concluding anything: this router layers anthropic_error_middleware, so it looked like this might already be handled. It is not. That middleware only rewrites 422 to 400, and neither rejection is a 422.

The fix reads the body explicitly and reports unsupported media type, oversized body and malformed JSON through anthropic_error. handler_count_tokens gains the HeaderMap it needs to see the content type.

Content-type parsing reuses is_json_content_type from the openai module rather than reimplementing it, so this route keeps the same notion of what counts as JSON, including parameters and +json suffixes. The policy is unchanged, only the envelope: an absent Content-Type is still rejected, matching ensure_json_content_type today.

Overlap worth flagging

Two things this touches are also touched elsewhere, so whoever merges second should expect a trivial rebase rather than a surprise:

This is the same class as #13685 and #13696 but a third module with a third envelope, so it is deliberately not a copy of either: Anthropic nests under {"type":"error","error":{...}}, generate uses {"error":{...}}, and the openai routes use a flat {message,type,code}.

Validation

Measured before and after by driving the route through spawn_service(|b| b.enable_anthropic_endpoints(true)):

input before after
Content-Type: text/plain 415 text/plain 415 application/json, {"type":"error","error":{"type":"invalid_request_error",...}}
{not json 400 text/plain 400 application/json, same envelope

Added test_anthropic_bad_content_type_returns_anthropic_envelope and test_anthropic_malformed_json_returns_anthropic_envelope, asserting the response content-type as well as the body, because the status codes were already correct on main and only the shape was wrong. Both confirmed red first by restoring anthropic.rs from upstream/main and re-running:

the 415 must use the Anthropic envelope, not Axum's text/plain rejection
the 400 must use the Anthropic envelope, not Axum's text/plain rejection

Not verified here: no GPU, so the success path of /v1/messages was not exercised end to end. This change only affects how the request body is read and how that read is reported when it fails.


Open in Devin Review

Summary by CodeRabbit

  • Bug Fixes
    • Anthropic Messages and token-count requests now provide consistent JSON-formatted error responses for unsupported content types, oversized request bodies, read failures, and malformed JSON.
    • Invalid requests now return the appropriate HTTP status codes, including 400 for malformed JSON and 415 for unsupported media types.
    • Request body size limits are enforced consistently across supported Anthropic endpoints.

`/v1/messages` and `/v1/messages/count_tokens` answer every failure they
produce themselves with the nested Anthropic envelope:

    {"type":"error","error":{"type":"invalid_request_error","message":"..."}}

ai-dynamo#13102 added the same envelope for unmatched Anthropic routes, noting that
Anthropic clients expect that shape. Requests that fail before reaching the
handler did not get it. Both handlers used Axum's `Json` extractor, whose
rejections are `text/plain`:

    Content-Type: text/plain  -> 415 text/plain  Expected request with `Content-Type: application/json`
    body "{not json"          -> 400 text/plain  Failed to parse the request body as JSON: ...

`anthropic_error_middleware` does not cover these. It only rewrites 422 to
400, and neither rejection is a 422.

Read the body explicitly and report unsupported media type, oversized body and
malformed JSON through `anthropic_error`. `handler_count_tokens` gains the
`HeaderMap` it needs to see the content type.

Content-type parsing reuses `is_json_content_type` from the openai module
rather than reimplementing it. The policy is unchanged, only the envelope: an
absent `Content-Type` is still rejected.

Signed-off-by: ayaangazali <ayaangazali.work@gmail.com>
@ayaangazali
ayaangazali requested a review from a team as a code owner August 23, 2026 19:20
Copilot AI lite review requested due to automatic review settings August 23, 2026 19:20
@copy-pr-bot

copy-pr-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@ayaangazali
ayaangazali temporarily deployed to external_collaborator August 23, 2026 19:20 — with GitHub Actions Inactive
@ayaangazali
ayaangazali temporarily deployed to external_collaborator August 23, 2026 19:20 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi ayaangazali! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added external-contribution Pull request is from an external contributor fix frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` labels Aug 23, 2026
@coderabbitai

coderabbitai Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 851a9d9b-9564-4a74-a620-ae7b03b88a17

📥 Commits

Reviewing files that changed from the base of the PR and between 004cd02 and 16da40f.

📒 Files selected for processing (3)
  • lib/llm/src/http/service/anthropic.rs
  • lib/llm/src/http/service/openai.rs
  • lib/llm/src/http/service/service_v2.rs

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


Walkthrough

Anthropic message and token-count handlers now use centralized request parsing. The parser validates JSON content types, enforces body limits, parses JSON, and returns Anthropic-formatted errors. Integration tests cover unsupported media types and malformed JSON.

Changes

Anthropic request handling

Layer / File(s) Summary
Shared request parser
lib/llm/src/http/service/anthropic.rs, lib/llm/src/http/service/openai.rs
The shared parser validates application/json, enforces the body-size limit, maps failures to HTTP statuses, and formats Anthropic error envelopes.
Handler integration
lib/llm/src/http/service/anthropic.rs
The messages and token-count handlers now parse headers and raw bodies through the shared Anthropic parser.
Integration validation
lib/llm/src/http/service/service_v2.rs
Tests verify JSON Anthropic error responses for unsupported content types and malformed JSON.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 16da4

This change makes malformed or unsupported Anthropic request bodies return the expected JSON error envelope without changing the existing status-code policy. No actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the implementation and validation, but it omits the required template sections and the required Related Issues confirmation or issue link. Add the Overview, Details, Where should the reviewer start?, and Related Issues sections, including an issue link or confirmation that no issue exists.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: Anthropic error envelopes for invalid request bodies.

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@ayaangazali

Copy link
Copy Markdown
Contributor Author

Heads up on a coupling between this and its two siblings, which is my fault for not checking when I opened them.

This PR, #13696 #13699 each add the same is_json_content_type helper to lib/llm/src/http/service/openai.rs. It is not on main, so all three introduce it. Whichever one you merge first, the other two stop applying: verified by applying each to current main and then checking the next, which fails at openai.rs:2179 in every ordering.

The route handlers themselves do not overlap (generate.rs, anthropic.rs, busy_threshold.rs), and the error envelopes differ per route, which is why they were split.

Two ways out, your call:

  1. Merge whichever you prefer and I rebase the other two the same day to drop the duplicate helper.
  2. I fold all three into a single PR, so the helper lands once and you review one diff instead of three.

Happy to do either, or to close two of them if you would rather see one.

@rmccorm4
rmccorm4 deployed to external_collaborator September 21, 2026 20:20 — with GitHub Actions Active
.and_then(|value| value.to_str().ok())
.is_some_and(is_json_content_type);
if !is_json {
return Err(anthropic_error(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Supply the required ErrorClass in all four new calls

anthropic_error now requires (ErrorClass, StatusCode, &str, &str), but the new calls at lines 163, 178, 187, and 196 supply only three arguments. cargo check --locked --offline -p dynamo-llm --no-default-features --lib fails with four E0061 errors at this head. Pass UnsupportedMedia, PayloadTooLarge, and InvalidRequest as appropriate so this compiles and records the correct local-failure classifications.

{
anthropic_error(
StatusCode::PAYLOAD_TOO_LARGE,
"invalid_request_error",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Use request_too_large for the 413 envelope

An oversized body currently emits HTTP 413 with error.type = "invalid_request_error". Both existing Anthropic mappings in this module map payload-too-large failures to "request_too_large", and anthropic_error passes the supplied string through unchanged. Use that type here and add a regression assertion for the oversized-body branch; the two new tests only cover 400 and 415.

}

/// Anthropic clients parse the nested `{"type": "error", "error": {...}}`
/// envelope, which #13102 established for unmatched routes. The Messages

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3, optional] Keep the comment focused on the contract

This repeats the PR history, including the previous extractor and #13102. Trim it to the behavior being asserted, or let the test name and assertions explain it. The same historical explanation above read_anthropic_request can be shortened to its first two lines.

This branch was successfully deployed

1 active deployment
external_collaborator — 28292cd9 Deployed Sep 21, 2026 by rmccorm4 via ok-to-test #21255
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request is from an external contributor fix frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants