Browse Source
Replace the per-request email.message.Message allocation in get_request_handler with a lightweight _is_json_content_type() helper that uses simple string operations (split, find, lower). Benchmarks show a ~5x speedup (80% time reduction) for content-type checking on every JSON body request. - Add _is_json_content_type() to fastapi/routing.py - Remove import email.message (no longer needed) - Add 26 parametrized unit tests for the new helper - Add benchmark script comparing old vs new approachpull/15629/head
3 changed files with 176 additions and 7 deletions
@ -0,0 +1,68 @@ |
|||
"""Benchmark: email.message.Message vs string-parsing for content-type detection.""" |
|||
|
|||
import email.message |
|||
import timeit |
|||
|
|||
from fastapi.routing import _is_json_content_type |
|||
|
|||
CONTENT_TYPES = [ |
|||
"application/json", |
|||
"application/json; charset=utf-8", |
|||
"application/geo+json", |
|||
"application/vnd.api+json", |
|||
"text/plain", |
|||
"application/xml", |
|||
"application/octet-stream", |
|||
"multipart/form-data; boundary=----", |
|||
"application/not-really-json", |
|||
"application/geo+json-seq", |
|||
] |
|||
|
|||
ITERATIONS = 100_000 |
|||
|
|||
|
|||
def old_is_json(content_type: str) -> bool: |
|||
"""Original implementation using email.message.Message.""" |
|||
message = email.message.Message() |
|||
message["content-type"] = content_type |
|||
if message.get_content_maintype() == "application": |
|||
subtype = message.get_content_subtype() |
|||
if subtype == "json" or subtype.endswith("+json"): |
|||
return True |
|||
return False |
|||
|
|||
|
|||
def bench(func, label: str) -> float: |
|||
def run(): |
|||
for ct in CONTENT_TYPES: |
|||
func(ct) |
|||
|
|||
elapsed = timeit.timeit(run, number=ITERATIONS) |
|||
ops = ITERATIONS * len(CONTENT_TYPES) |
|||
rate = ops / elapsed |
|||
print(f" {label:30s} {elapsed:8.3f}s ({rate:,.0f} ops/s)") |
|||
return elapsed |
|||
|
|||
|
|||
def main() -> None: |
|||
# Verify both implementations agree on all inputs |
|||
for ct in CONTENT_TYPES: |
|||
assert old_is_json(ct) == _is_json_content_type(ct), ( |
|||
f"Mismatch on {ct!r}: old={old_is_json(ct)}, new={_is_json_content_type(ct)}" |
|||
) |
|||
|
|||
print( |
|||
f"\nBenchmark: {ITERATIONS:,} iterations x {len(CONTENT_TYPES)} content-types\n" |
|||
) |
|||
|
|||
old_time = bench(old_is_json, "email.message (old)") |
|||
new_time = bench(_is_json_content_type, "string parsing (new)") |
|||
|
|||
speedup = old_time / new_time |
|||
pct = (1 - new_time / old_time) * 100 |
|||
|
|||
print(f"\n Speedup: {speedup:.1f}x faster ({pct:.1f}% reduction in time)") |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,76 @@ |
|||
import pytest |
|||
from fastapi.routing import _is_json_content_type |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"content_type", |
|||
[ |
|||
"application/json", |
|||
"application/JSON", |
|||
"Application/JSON", |
|||
"APPLICATION/JSON", |
|||
"application/json; charset=utf-8", |
|||
"application/json;charset=utf-8", |
|||
"application/json ; charset=utf-8", |
|||
"application/geo+json", |
|||
"application/vnd.api+json", |
|||
"application/vnd.example.api+json", |
|||
"application/vnd.api+json; charset=utf-8", |
|||
" application/json ", |
|||
], |
|||
ids=[ |
|||
"plain", |
|||
"upper-subtype", |
|||
"mixed-case", |
|||
"all-upper", |
|||
"with-charset", |
|||
"charset-no-space", |
|||
"charset-extra-space", |
|||
"geo+json", |
|||
"vnd+json", |
|||
"nested-vnd+json", |
|||
"vnd+json-with-charset", |
|||
"surrounding-whitespace", |
|||
], |
|||
) |
|||
def test_json_content_types_accepted(content_type: str) -> None: |
|||
assert _is_json_content_type(content_type) is True |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"content_type", |
|||
[ |
|||
"text/plain", |
|||
"text/html", |
|||
"multipart/form-data", |
|||
"application/xml", |
|||
"application/octet-stream", |
|||
"application/not-really-json", |
|||
"application/geo+json-seq", |
|||
"application/jsonl", |
|||
"application/x-ndjson", |
|||
"json", |
|||
"", |
|||
"application", |
|||
"/json", |
|||
"application/", |
|||
], |
|||
ids=[ |
|||
"text-plain", |
|||
"text-html", |
|||
"multipart", |
|||
"xml", |
|||
"octet-stream", |
|||
"not-really-json", |
|||
"json-seq", |
|||
"jsonl", |
|||
"ndjson", |
|||
"no-slash", |
|||
"empty", |
|||
"no-subtype", |
|||
"no-maintype", |
|||
"trailing-slash", |
|||
], |
|||
) |
|||
def test_non_json_content_types_rejected(content_type: str) -> None: |
|||
assert _is_json_content_type(content_type) is False |
|||
Loading…
Reference in new issue