From harness-claude
Guides HTTP content negotiation for APIs to serve JSON, XML, CSV, or versioned media types via Accept header without URL versioning. Useful for multi-format endpoints, 406 errors, and Vary caching.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> CONTENT NEGOTIATION IS THE HTTP MECHANISM BY WHICH CLIENTS AND SERVERS AGREE ON THE FORMAT, LANGUAGE, AND ENCODING OF A RESPONSE — ENABLING A SINGLE ENDPOINT TO SERVE JSON, XML, CSV, OR VERSIONED MEDIA TYPES WITHOUT SEPARATE URLS. IGNORING CONTENT NEGOTIATION FORCES VERSIONING THROUGH URLS OR QUERY PARAMETERS AND MAKES FORMAT DISCOVERY OPAQUE.
Guides API versioning using HTTP headers like Accept, vendor media types, and custom API-Version instead of URI paths for clean, RESTful resource URIs and fine-grained control as in Stripe and GitHub.
Guides designing REST and GraphQL APIs for headless CMS content delivery, including preview endpoints, localization, pagination, filtering, caching headers, and versioning.
Guides API versioning strategies including URL path, header, query parameter, and content negotiation. Helps manage breaking changes, deprecations, and multiple versions.
Share bugs, ideas, or general feedback.
CONTENT NEGOTIATION IS THE HTTP MECHANISM BY WHICH CLIENTS AND SERVERS AGREE ON THE FORMAT, LANGUAGE, AND ENCODING OF A RESPONSE — ENABLING A SINGLE ENDPOINT TO SERVE JSON, XML, CSV, OR VERSIONED MEDIA TYPES WITHOUT SEPARATE URLS. IGNORING CONTENT NEGOTIATION FORCES VERSIONING THROUGH URLS OR QUERY PARAMETERS AND MAKES FORMAT DISCOVERY OPAQUE.
application/vnd.myapp.v2+json)406 Not Acceptable error from a client or proxy/v2/users) and header versioning (Accept: application/vnd.api.v2+json)Content-Type: application/json without honoring the Accept headerAccept headerAccept Header (Client-Driven Negotiation) — The client advertises acceptable response media types in order of preference using quality factors (q=). The server selects the best match and responds with the chosen type in Content-Type. If no acceptable type is available, the server returns 406 Not Acceptable.
GET /reports/q1-2024
Accept: text/csv;q=0.9, application/json;q=1.0, */*;q=0.1
The server reads this as: JSON preferred (q=1.0), CSV acceptable (q=0.9), anything else as last resort.
Content-Type Header — Declares the media type of the request body (on POST/PUT/PATCH) or response body. The client sets it on requests with bodies; the server sets it on responses. Mismatch between declared and actual type causes parsing failures.
POST /events
Content-Type: application/json
{ "type": "order.completed", "orderId": "ord_123" }
Media Types and Vendor Types — Media types follow the pattern type/subtype[+suffix][;parameter]. Vendor types (application/vnd.*) allow APIs to declare version-specific or format-specific contracts. For example, application/vnd.github.v3+json is GitHub's versioned JSON type. The +json suffix tells generic parsers they can treat the body as JSON even without specific type knowledge.
Quality Factors (q values) — Values from 0.0 to 1.0 indicating relative preference. Default is 1.0. q=0 means "not acceptable." Used in Accept, Accept-Language, Accept-Encoding, and Accept-Charset headers. Servers must implement negotiation logic that respects q-value ordering.
Accept: application/json;q=1.0, application/xml;q=0.8, text/plain;q=0.5
Vary Header — Tells downstream caches (CDNs, proxies, browsers) which request headers were used in content negotiation. A response that varies by Accept must include Vary: Accept. Without this, a CDN may serve a JSON response to a client requesting CSV if both requests hit the same cache key.
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept, Accept-Language
Accept-Encoding and Compression — Clients declare supported compression algorithms; servers respond with compressed bodies and Content-Encoding headers. gzip and br (Brotli) are the most common. Compression negotiation is separate from format negotiation.
GET /large-dataset
Accept-Encoding: br, gzip;q=0.8
HTTP/1.1 200 OK
Content-Encoding: br
Content-Type: application/json
GitHub's API demonstrates media-type versioning through content negotiation. GitHub uses Accept headers both for version selection and for enabling preview features:
Request the default v3 JSON response:
GET /repos/octocat/hello-world
Authorization: Bearer ghp_...
Accept: application/vnd.github.v3+json
HTTP/1.1 200 OK
Content-Type: application/vnd.github.v3+json
Vary: Accept, Authorization
X-GitHub-Media-Type: github.v3; format=json
{
"id": 1296269,
"name": "hello-world",
"full_name": "octocat/hello-world",
...
}
Request raw file content (format negotiation, same endpoint):
GET /repos/octocat/hello-world/contents/README.md
Accept: application/vnd.github.raw+json
HTTP/1.1 200 OK
Content-Type: text/plain
Vary: Accept
# Hello World
...
Enable a preview feature via Accept header (GitHub Reaction preview):
GET /repos/octocat/hello-world/issues/1
Accept: application/vnd.github.squirrel-girl-preview+json
The same URL returns an augmented response with reactions field when the preview media type is requested. This is GitHub's mechanism for progressive feature rollout without URL proliferation.
406 Not Acceptable — requesting an unsupported type:
GET /repos/octocat/hello-world
Accept: application/x-yaml
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{ "message": "Must accept 'application/vnd.github.v3+json'" }
Ignoring the Accept header and always returning JSON. A server that returns Content-Type: application/json regardless of the Accept header breaks negotiation. If the client requests Accept: application/xml and receives JSON, it either rejects the response or silently parses wrong data. Fix: check the Accept header, return the negotiated type, and return 406 Not Acceptable if no acceptable type is available.
URL-based format selection instead of content negotiation. Adding /users.json and /users.xml as separate endpoints duplicates routing, skips the Vary header (breaking CDN cache correctness), and adds URL surface area. HTTP already provides the mechanism: use Accept headers and vary cache responses accordingly.
Omitting the Vary header on negotiated responses. A CDN that caches a JSON response without seeing Vary: Accept will serve that cached JSON to all subsequent requests for the same URL — including clients requesting CSV. The Vary header is mandatory whenever response content differs based on request headers.
Media-type versioning without a default. If an API requires Accept: application/vnd.myapp.v2+json but provides no fallback for plain Accept: application/json, existing clients that omit the vendor type receive a 406. Always define a default version for generic JSON requests, documented in the API contract.
| Approach | Example | Pros | Cons |
|---|---|---|---|
| URL versioning | /v2/users | Simple, visible, bookmarkable | URL proliferation, breaking resources |
| Query param | /users?version=2 | Simple | Caching issues, not RESTful |
| Accept header | Accept: application/vnd.api.v2+json | Clean URLs, proper HTTP | Less visible, harder to test in browser |
| Custom header | Api-Version: 2 | Simple | Non-standard, not cached by Vary |
Media-type versioning via Accept is the most RESTful but requires CDN and proxy configuration for correct Vary handling. Most public APIs (Stripe, GitHub, Twilio) choose URL versioning for its simplicity and developer experience.
Twilio's REST API accepts both application/json and application/x-www-form-urlencoded on request bodies (via Content-Type) and returns JSON by default. When Twilio added support for CSV exports on call logs, they used content negotiation rather than a separate /export endpoint:
GET /2010-04-01/Accounts/{AccountSid}/Calls.json
Accept: text/csv
Returns a CSV download of the same resource. The Vary: Accept header ensures CDN caches do not mix JSON and CSV responses. This avoided a URL proliferation problem that had plagued the earlier /Calls.json vs /Calls.xml pattern (which duplicated the file-extension suffix hack).
Accept header parsing in the server: parse quality factors, find the best match against supported types, return 406 if no match.Content-Type in every response to the exact negotiated media type (including vendor type if applicable).Vary headers listing all request headers used in negotiation (Accept, Accept-Language, Accept-Encoding).harness validate to confirm skill files are well-formed.Accept header and returns the best-match media type in Content-Type.406 Not Acceptable is returned when no client-acceptable type is available.Vary header listing those headers.application/vnd.*+json) and documents a default for generic application/json requests.Accept-Encoding is honored for compression, with Content-Encoding set in compressed responses.