Skip to content

Accepts Middleware ​

A comprehensive content negotiation middleware for the Nexios ASGI framework that handles Accept headers and provides intelligent content negotiation.

It automatically:

  • Parses and processes Accept, Accept-Language, Accept-Charset, and Accept-Encoding headers
  • Performs intelligent content negotiation based on client preferences
  • Sets appropriate Vary headers for proper caching
  • Provides utilities for manual content negotiation
  • Supports strict content negotiation with 406 Not Acceptable responses

Installation ​

bash
pip install nexios_contrib

Quick Start ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import Accepts

app = NexiosApp()

# Add accepts middleware
app.add_middleware(Accepts())

@app.get("/")
async def home(request, response):
    # Access parsed accept headers
    accepts_info = getattr(request, 'accepts', {})
    accept_header = accepts_info.get('raw_accept', '')

    return {
        "message": "Hello with Content Negotiation!",
        "accepts": accept_header
    }

Content Negotiation Examples ​

Basic Usage ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import Accepts

app = NexiosApp()

app.add_middleware(Accepts())

@app.get("/api/data")
async def get_data(request, response):
    # The middleware automatically handles content negotiation
    # Response content-type will be negotiated based on Accept header

    data = {"message": "API data", "items": [1, 2, 3]}

    # Client sends: Accept: application/json
    # Response will have: Content-Type: application/json

    return data

Manual Content Negotiation ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import ContentNegotiationMiddleware, negotiate_content_type

app = NexiosApp()

app.add_middleware(ContentNegotiationMiddleware())

@app.get("/api/content")
async def get_content(request, response):
    # Manual content negotiation
    available_types = ["application/json", "application/xml", "text/html"]
    best_type = negotiate_content_type(
        request.headers.get('Accept', ''),
        available_types
    )

    response.headers['Content-Type'] = best_type

    if best_type == "application/json":
        return {"data": "JSON format"}
    elif best_type == "application/xml":
        return "<data>XML format</data>"
    else:
        return "<h1>HTML format</h1>"

Language Negotiation ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import ContentNegotiationMiddleware, negotiate_language

app = NexiosApp()

app.add_middleware(ContentNegotiationMiddleware())

@app.get("/greetings")
async def get_greetings(request, response):
    available_languages = ["en", "es", "fr", "de"]
    best_language = negotiate_language(
        request.headers.get('Accept-Language', ''),
        available_languages
    )

    greetings = {
        "en": "Hello",
        "es": "Hola",
        "fr": "Bonjour",
        "de": "Guten Tag"
    }

    return {
        "greeting": greetings.get(best_language, greetings["en"]),
        "language": best_language
    }

Strict Content Negotiation ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import StrictContentNegotiationMiddleware

app = NexiosApp()

app.add_middleware(
    StrictContentNegotiationMiddleware(
        available_types=["application/json", "application/xml"],
        available_languages=["en", "es"]
    )
)

@app.get("/api/strict")
async def strict_api(request, response):
    # Will return 406 Not Acceptable if client doesn't accept JSON or XML
    # and doesn't accept English or Spanish

    negotiated_type = getattr(request, 'negotiated_content_type', 'application/json')
    negotiated_lang = getattr(request, 'negotiated_language', 'en')

    return {
        "data": "Strict API response",
        "content_type": negotiated_type,
        "language": negotiated_lang
    }

Configuration Options ​

AcceptsMiddleware Options ​

OptionTypeDefaultDescription
default_content_typestr"application/json"Default content type when no match found
default_languagestr"en"Default language when no match found
default_charsetstr"utf-8"Default charset when no match found
set_vary_headerboolTrueAutomatically set Vary headers
store_accepts_infoboolTrueStore parsed accepts info in request object

StrictContentNegotiationMiddleware Options ​

OptionTypeDefaultDescription
available_typesList[str]RequiredContent types this application can serve
available_languagesList[str]["en"]Languages this application supports

Usage Examples ​

Basic Setup ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import Accepts

app = NexiosApp()

app.add_middleware(Accepts())

Custom Configuration ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import Accepts

app = NexiosApp()

app.add_middleware(
    Accepts(
        default_content_type="application/json",
        default_language="en",
        default_charset="utf-8",
        set_vary_header=True,
        store_accepts_info=True
    )
)

Accessing Accept Information ​

python
@app.get("/debug/accepts")
async def debug_accepts(request, response):
    accepts = getattr(request, 'accepts', {})

    return {
        "accept_header": accepts.get('raw_accept', ''),
        "accepted_types": [item.value for item in accepts.get('accept', [])],
        "accept_language": accepts.get('raw_accept_language', ''),
        "accepted_languages": [item.value for item in accepts.get('accept_language', [])],
        "accept_charset": accepts.get('raw_accept_charset', ''),
        "accept_encoding": accepts.get('raw_accept_encoding', ''),
    }

Content Negotiation Utilities ​

python
from nexios_contrib.accepts import get_best_match, negotiate_content_type

@app.get("/negotiate")
async def negotiate_example(request, response):
    # Get best match from options
    best_type = get_best_match(
        request.headers.get('Accept', ''),
        ["application/json", "text/html", "application/xml"]
    )

    # Or use specific negotiation
    json_type = negotiate_content_type(
        request.headers.get('Accept', ''),
        ["application/json", "application/vnd.api+json"]
    )

    return {
        "best_match": best_type,
        "json_match": json_type,
        "accept_header": request.headers.get('Accept', '')
    }

Supported Headers ​

Input Headers (Parsed) ​

  • Accept: Media types (e.g., text/html, application/json;q=0.9)
  • Accept-Language: Languages (e.g., en-US, en;q=0.9, es;q=0.8)
  • Accept-Charset: Character sets (e.g., utf-8, iso-8859-1;q=0.9)
  • Accept-Encoding: Content encodings (e.g., gzip, deflate, br)

Output Headers (Set) ​

  • Vary: Automatically set to include relevant Accept headers
  • Content-Type: Automatically negotiated based on Accept header

Content Negotiation Algorithm ​

Media Type Matching ​

The middleware follows RFC 7231 content negotiation rules:

  1. Exact Match: application/json matches application/json
  2. Type Match: text/* matches text/html, text/plain
  3. Wildcard Match: */* matches any media type
  4. Quality Factor: Items are sorted by q-value (0.0 to 1.0)
  5. Specificity: More specific types are preferred over generic ones

Language Matching ​

Language negotiation follows RFC 7231:

  1. Exact Match: en-US matches en-US
  2. Prefix Match: en matches en-US, en-GB
  3. Fallback: First available language if no match

Helper Functions ​

Parsing Accept Headers ​

python
from nexios_contrib.accepts import parse_accept_header, parse_accept_language

# Parse Accept header
accept_items = parse_accept_header("text/html, application/json;q=0.9")
# Returns: [AcceptItem("text/html", 1.0), AcceptItem("application/json", 0.9)]

# Parse Accept-Language header
lang_items = parse_accept_language("en-US, en;q=0.9, es;q=0.8")
# Returns: [AcceptItem("en-US", 1.0), AcceptItem("en", 0.9), AcceptItem("es", 0.8)]

Content Negotiation ​

python
from nexios_contrib.accepts import negotiate_content_type, negotiate_language

# Content type negotiation
best_type = negotiate_content_type(
    "text/html, application/json;q=0.9",
    ["application/json", "text/html", "application/xml"]
)
# Returns: "text/html" (exact match wins over quality)

# Language negotiation
best_lang = negotiate_language(
    "en-US, fr;q=0.8",
    ["en", "fr", "es"]
)
# Returns: "en" (prefix match)

Best Match Selection ​

python
from nexios_contrib.accepts import get_best_match

# Get best match from options
best = get_best_match(
    "application/json, text/html;q=0.9",
    ["text/html", "application/json", "application/xml"]
)
# Returns: "application/json" (first in list with highest quality)

Advanced Usage ​

Custom Content Negotiation ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import ContentNegotiationMiddleware

class CustomNegotiationMiddleware(ContentNegotiationMiddleware):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.custom_types = {
            "application/vnd.api.v1+json": ["application/json"],
            "application/vnd.api.v2+json": ["application/json", "application/vnd.api.v1+json"]
        }

    def negotiate_content_type(self, request, available_types, default_type=None):
        accept_header = request.headers.get('Accept', '')

        # Check for custom vendor types
        for custom_type, fallback_types in self.custom_types.items():
            if custom_type in accept_header:
                for fallback in fallback_types:
                    if fallback in available_types:
                        return fallback

        # Fall back to standard negotiation
        return super().negotiate_content_type(request, available_types, default_type)

app = NexiosApp()
app.add_middleware(CustomNegotiationMiddleware())

API Versioning with Content Negotiation ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import ContentNegotiationMiddleware

app = NexiosApp()
app.add_middleware(ContentNegotiationMiddleware())

@app.get("/api/users")
async def get_users(request, response):
    # Support API versioning through Accept header
    accept_header = request.headers.get('Accept', '')

    if 'application/vnd.myapi.v2+json' in accept_header:
        response.headers['Content-Type'] = 'application/vnd.myapi.v2+json'
        return {"users": [], "version": "v2", "features": ["new_field"]}
    else:
        response.headers['Content-Type'] = 'application/vnd.myapi.v1+json'
        return {"users": [], "version": "v1"}

Internationalization (i18n) ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import ContentNegotiationMiddleware, negotiate_language

app = NexiosApp()
app.add_middleware(ContentNegotiationMiddleware())

@app.get("/messages")
async def get_messages(request, response):
    available_messages = {
        "en": {"greeting": "Hello", "farewell": "Goodbye"},
        "es": {"greeting": "Hola", "farewell": "Adiós"},
        "fr": {"greeting": "Bonjour", "farewell": "Au revoir"}
    }

    best_lang = negotiate_language(
        request.headers.get('Accept-Language', ''),
        list(available_messages.keys())
    )

    messages = available_messages.get(best_lang, available_messages["en"])
    response.headers['Content-Language'] = best_lang

    return messages

Best Practices ​

  1. Always set Vary headers when using content negotiation for proper caching
  2. Provide sensible defaults for content type and language
  3. Test with real browsers - different browsers send different Accept headers
  4. Use strict negotiation for APIs that only support specific content types
  5. Cache negotiated responses - use Vary headers to ensure proper cache keys
  6. Support multiple formats for better API compatibility

Example Production Configuration ​

python
from nexios import NexiosApp
from nexios_contrib.accepts import StrictContentNegotiationMiddleware

app = NexiosApp()

app.add_middleware(
    StrictContentNegotiationMiddleware(
        available_types=[
            "application/json",
            "application/vnd.api.v1+json",
            "application/vnd.api.v2+json"
        ],
        available_languages=["en", "es", "fr", "de"],
        default_content_type="application/json",
        default_language="en"
    )
)

@app.middleware
async def content_negotiation_logger(request, response, call_next):
    negotiated_type = getattr(request, 'negotiated_content_type', 'unknown')
    negotiated_lang = getattr(request, 'negotiated_language', 'unknown')

    print(f"Content-Type: {negotiated_type}, Language: {negotiated_lang}")

    return await call_next()

Common Issues ​

Content-Type Not Being Set ​

If Content-Type headers aren't being set automatically:

  1. Check that Accepts middleware is added to your app
  2. Verify the Accept header is being sent by the client
  3. Ensure default_content_type is set correctly

Vary Headers Too Broad ​

If Vary headers include too many fields:

  1. Set set_vary_header=False if you don't need automatic Vary headers
  2. Manually set specific Vary headers as needed
  3. Consider caching implications when using content negotiation

Language Fallback Issues ​

If language negotiation isn't working as expected:

  1. Verify Accept-Language header is being sent
  2. Check that your available languages list is correct
  3. Ensure proper language codes (e.g., "en-US", not "en_US")

Troubleshooting ​

Debug Accept Headers ​

python
@app.get("/debug/headers")
async def debug_headers(request, response):
    return {
        "accept": request.headers.get('Accept'),
        "accept_language": request.headers.get('Accept-Language'),
        "accept_charset": request.headers.get('Accept-Charset'),
        "accept_encoding": request.headers.get('Accept-Encoding'),
        "user_agent": request.headers.get('User-Agent')
    }

Test Content Negotiation ​

python
# Test script to verify content negotiation
import requests

# Test different Accept headers
headers_tests = [
    {"Accept": "application/json"},
    {"Accept": "text/html"},
    {"Accept": "application/xml"},
    {"Accept": "text/html, application/json;q=0.9"},
    {"Accept-Language": "en-US, fr;q=0.8"}
]

for headers in headers_tests:
    response = requests.get("http://localhost:8000/api/content", headers=headers)
    print(f"Headers: {headers}")
    print(f"Response Content-Type: {response.headers.get('Content-Type')}")
    print(f"Response: {response.text}\n")

Built with ❤️ by the @nexios-labs community.