Skip to content

API version negotiation should support Accept parameters and multiple media ranges #341

Description

@Kewe63

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:

  1. split or parse the comma-separated entry safely;
  2. parse the media type and parameters;
  3. ignore ranges with q=0;
  4. recognize supported Arc vendor media types, application/json, and */*;
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions