Summary
The consensus REST API's version negotiation rejects valid Accept headers when a supported media type includes a standard parameter or appears in a comma-separated media-range list.
For example, both of these requests currently receive HTTP 406 even though they offer the supported v1 representation:
Accept: application/vnd.arc.v1+json; q=0.9
Accept: text/html, application/vnd.arc.v1+json
HTTP Accept is a list of media ranges, and each range can carry parameters such as a quality value. The version parser currently compares the complete header string as if it were one bare media type.
Affected files
crates/malachite-app/src/rpc/version.rs
crates/malachite-app/src/rpc/middleware.rs
Observed behavior
ApiVersion::from_accept_header() trims the complete header and then performs exact comparisons or prefix/suffix matching against that whole string:
if trimmed.is_empty() || trimmed == MEDIA_TYPE_JSON || trimmed == MEDIA_TYPE_ANY {
return Some(Self::default());
}
if let Some(version_part) = trimmed.strip_prefix(MEDIA_TYPE_PREFIX) {
if let Some(version_str) = version_part.strip_suffix("+json") {
return ApiVersion::from_str(version_str).ok();
}
}
Consequently:
ApiVersion::from_accept_header("application/vnd.arc.v1+json; q=0.9")
returns None because the value no longer ends with +json.
Likewise:
ApiVersion::from_accept_header("text/html, application/vnd.arc.v1+json")
returns None because the complete value does not start with the Arc media-type prefix.
extract_version() treats None as an unsupported version and returns StatusCode::NOT_ACCEPTABLE.
Expected behavior
Version negotiation should examine the individual media ranges in the Accept field and select a supported, acceptable representation.
At minimum:
application/vnd.arc.v1+json; q=0.9 should negotiate v1;
text/html, application/vnd.arc.v1+json should negotiate v1;
- equivalent lists containing
application/json or */* should continue to negotiate the default version;
- unsupported versioned media types should continue to produce HTTP 406 when no supported range is acceptable.
Quality value q=0 should not make a representation acceptable.
Reproduction
I added two focused middleware regression tests against current main. They build a minimal Axum router with the production extract_version middleware and send requests with the affected headers:
#[tokio::test]
async fn test_accept_header_with_parameters() {
assert_eq!(
status_for_accept("application/vnd.arc.v1+json; q=0.9").await,
StatusCode::OK
);
}
#[tokio::test]
async fn test_accept_header_with_multiple_ranges() {
assert_eq!(
status_for_accept("text/html, application/vnd.arc.v1+json").await,
StatusCode::OK
);
}
Command:
cargo +1.94.0 test \
-p arc-node-consensus \
test_accept_header_with_ \
-- --nocapture
Result:
running 2 tests
rpc::middleware::tests::test_accept_header_with_parameters ... FAILED
rpc::middleware::tests::test_accept_header_with_multiple_ranges ... FAILED
left: 406
right: 200
test result: FAILED. 0 passed; 2 failed; 402 filtered out
The underlying parser behavior was also reproduced directly against version.rs:
application/vnd.arc.v1+json; q=0.9
left: None
right: Some(V1)
text/html, application/vnd.arc.v1+json
left: None
right: Some(V1)
Tested against commit:
97f8da0dc4faa703fe2d68ca007e40dab2c8a9ef
Root cause
from_accept_header() treats the entire Accept field value as a single exact media type. It does not parse:
- comma-separated media ranges;
- media-range parameters;
- quality values.
The resulting None is then mapped directly to HTTP 406 by the middleware.
Why this matters
Standards-compliant HTTP clients, proxies, SDKs, and generic API tooling commonly send weighted or multi-value Accept fields. Those clients can be rejected even when they explicitly include an Arc-supported representation.
This makes the API less interoperable and turns valid content-negotiation syntax into an unsupported-version response.
RFC 9110 section 12.5.1 defines Accept as a list of media ranges with optional parameters and quality values:
https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1
Suggested fix
Parse the Accept field as a list of media ranges rather than matching the raw field value.
For each range:
- split or parse the comma-separated entry safely;
- parse the media type and parameters;
- ignore ranges with
q=0;
- recognize supported Arc vendor media types,
application/json, and */*;
- return HTTP 406 only when no acceptable supported range remains.
Using an established HTTP media-type/Accept parser would avoid reimplementing quoted parameter and quality-value edge cases.
Potential regression tests
Add parser and middleware tests covering:
- vendor media type with a positive
q value;
- generic JSON with a positive
q value;
- a supported media type after an unsupported range;
- wildcard fallback in a media-range list;
- whitespace around commas and parameters;
- supported ranges with
q=0;
- only unsupported versioned media types;
- malformed quality values.
Duplicate check
I searched open and closed issues and pull requests using combinations of:
Accept header
media ranges
version negotiation
application/json
406
NOT_ACCEPTABLE
from_accept_header
No direct duplicate or existing implementation was found.
Summary
The consensus REST API's version negotiation rejects valid
Acceptheaders when a supported media type includes a standard parameter or appears in a comma-separated media-range list.For example, both of these requests currently receive HTTP 406 even though they offer the supported v1 representation:
HTTP
Acceptis a list of media ranges, and each range can carry parameters such as a quality value. The version parser currently compares the complete header string as if it were one bare media type.Affected files
crates/malachite-app/src/rpc/version.rscrates/malachite-app/src/rpc/middleware.rsObserved behavior
ApiVersion::from_accept_header()trims the complete header and then performs exact comparisons or prefix/suffix matching against that whole string:Consequently:
returns
Nonebecause the value no longer ends with+json.Likewise:
returns
Nonebecause the complete value does not start with the Arc media-type prefix.extract_version()treatsNoneas an unsupported version and returnsStatusCode::NOT_ACCEPTABLE.Expected behavior
Version negotiation should examine the individual media ranges in the
Acceptfield and select a supported, acceptable representation.At minimum:
application/vnd.arc.v1+json; q=0.9should negotiate v1;text/html, application/vnd.arc.v1+jsonshould negotiate v1;application/jsonor*/*should continue to negotiate the default version;Quality value
q=0should not make a representation acceptable.Reproduction
I added two focused middleware regression tests against current
main. They build a minimal Axum router with the productionextract_versionmiddleware and send requests with the affected headers:Command:
cargo +1.94.0 test \ -p arc-node-consensus \ test_accept_header_with_ \ -- --nocaptureResult:
The underlying parser behavior was also reproduced directly against
version.rs:Tested against commit:
Root cause
from_accept_header()treats the entireAcceptfield value as a single exact media type. It does not parse:The resulting
Noneis then mapped directly to HTTP 406 by the middleware.Why this matters
Standards-compliant HTTP clients, proxies, SDKs, and generic API tooling commonly send weighted or multi-value
Acceptfields. Those clients can be rejected even when they explicitly include an Arc-supported representation.This makes the API less interoperable and turns valid content-negotiation syntax into an unsupported-version response.
RFC 9110 section 12.5.1 defines
Acceptas a list of media ranges with optional parameters and quality values:https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1
Suggested fix
Parse the
Acceptfield as a list of media ranges rather than matching the raw field value.For each range:
q=0;application/json, and*/*;Using an established HTTP media-type/Accept parser would avoid reimplementing quoted parameter and quality-value edge cases.
Potential regression tests
Add parser and middleware tests covering:
qvalue;qvalue;q=0;Duplicate check
I searched open and closed issues and pull requests using combinations of:
Accept headermedia rangesversion negotiationapplication/json406NOT_ACCEPTABLEfrom_accept_headerNo direct duplicate or existing implementation was found.