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