Fast API with pytest using AsyncClient gives 422 on post? - python

I'm trying to send a request to an api using pytest through httpx.AsynClient
#pytest.mark.anyio
async def test_device_create_with_data(self, client, random_uuid):
device_create = DeviceCreateFactory.build(subId=random_uuid)
json = device_create.json(by_alias=True)
response = await client.post("/device", json=json)
assert response.status_code == 200
Client fixture:
from httpx import AsyncClient
#pytest.fixture(scope="session")
async def client():
async with AsyncClient(
app=app,
base_url="http://test/api/pc",
headers={"Content-Type": "application/json"}
) as client:
yield client
API endpoint:
#device_router.post("/device", response_model=CommonResponse)
async def create_device(device: DeviceCreate):
_, err = await crud_device.create_device(device)
if err:
return get_common_response(400, err)
return get_common_response(200, "ok")
Schemas:
class DeviceBase(BaseModel):
device_id: StrictStr = Field(..., alias='deviceId')
device_name: StrictStr = Field(..., alias='deviceName')
device_type: StrictStr = Field(..., alias='deviceType')
age_mode: AgeModeType = Field(..., alias='ageMode')
class Config:
allow_population_by_field_name = True
validate_all = True
validate_assignment = True
class DeviceCreate(DeviceBase):
sub_id: StrictStr = Field(..., alias='subId')
class Config:
orm_mode = True
Factory:
from pydantic_factories import ModelFactory
from app.core.schemas.device import DeviceCreate
class DeviceCreateFactory(ModelFactory):
__model__ = DeviceCreate
And i'm getting a 422 error with following response content:
"message":"bad request","details":{"deviceId":"field required","deviceName":"field required","deviceType":"field required","ageMode":"field required","subId":"field required"}
Then i examined the data of the request being sent and got:
b'"{\\"deviceId\\": \\"de\\", \\"deviceName\\": \\"\\", \\"deviceType\\": \\"\\", \\"ageMode\\": \\"child\\", \\"subId\\": \\"11aded61-9966-4be1-a781-387f75346811\\"}"'
Seems like everything is okay, but where is the trouble then?
I've tried to examine the request data in exception handler of 422
I've made:
#app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
print(await request.json())
response = validation_error_response(exc)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder(response.dict())
)
But code after print is unreachable, because await request.json() never ends and runs forever trying to print a request json
Is there a way to manage this problem?
Thanks for any suggest!
P.S.
python version: 3.8.9
fastapi version: 0.68.1
httpx version: 0.21.1

You're double encoding your content as JSON - you're both asking for it to be returned as a JSON string, and then telling your request method to encode it as JSON a second time. json= as an argument to the method on the client converts the given data to JSON - it does not expect already serialized JSON.
You can see this in your request string because it starts with " and not with { as you'd expect:
b'"{\
^
Instead build your model around a dictionary - or as I'd prefer in a test - build the request by hand, so that you're testing how you imagine an actual request should look like.
You can use dict in the same was as you'd use json for a Pydantic model:
device_create = DeviceCreateFactory.build(subId=random_uuid)
response = await client.post("/device", json=device_create.dict(by_alias=True))
assert response.status_code == 200

Related

Gateway Time-out with StreamingResponse and custom Middleware fastapi [duplicate]

We are writing a web service using Python FastAPI that is going to be hosted in Kubernetes. For auditing purposes, we need to save the raw JSON body of the request/response for specific routes. The body size of both request and response JSON is about 1MB, and preferably, this should not impact the response time.
How can we do that?
Option 1 - Using Middleware
You could use a Middleware. A middleware takes each request that comes to your application, and hence, allows you to handle the request before it is processed by any specific endpoint, as well as the response, before it is returned to the client. To create a middleware, you use the decorator #app.middleware("http") on top of a function, as shown below. As you need to consume the request body from the stream inside the middleware—using either request.body() or request.stream(), as shown in this answer (behind the scenes, the former method actually calls the latter, see here)—then it won't be available when you later pass the request to the corresponding endpoint. Thus, you can follow the approach described in this post to make the request body available down the line (i.e., using the set_body function below). As for the response body, you can use the same approach as described in this answer to consume the body and then return the response to the client. Either option described in the aforementioned linked answer would work; the below, however, uses Option 2, which stores the body in a bytes object and returns a custom Response directly (along with the status_code, headers and media_type of the original response).
To log the data, you could use a BackgroundTask, as described in this answer and this answer. A BackgroundTask will run only once the response has been sent (see Starlette documentation as well); thus, the client won't have to be waiting for the logging to complete before receiving the response (and hence, the response time won't be noticeably impacted).
Note
If you had a streaming request or response with a body that wouldn't fit into your server's RAM (for example, imagine a body of 100GB on a machine running 8GB RAM), it would become problematic, as you are storing the data to RAM, which wouldn't have enough space available to accommodate the accumulated data. Also, in case of a large response (e.g., a large FileResponse or StreamingResponse), you may be faced with Timeout errors on client side (or on reverse proxy side, if you are using one), as you would not be able to respond back to the client, until you have read the entire response body (as you are looping over response.body_iterator). You mentioned that "the body size of both request and response JSON is about 1MB"; hence, that should normally be fine (however, it is always a good practice to consider beforehand matters, such as how many requests your API is expected to be serving concurrently, what other applications might be using the RAM, etc., in order to rule whether this is an issue or not). If you needed to, you could limit the number of requests to your API endpoints using, for example, SlowAPI (as shown in this answer).
Limiting the usage of the middleware to specific routes only
You could limit the usage of the middleware to specific endpoints by:
checking the request.url.path inside the middleware against a
pre-defined list of routes for which you would like to log the
request and response, as described in this answer (see
"Update" section),
or using a sub application, as demonstrated in this
answer
or using a custom APIRoute class, as demonstrated in Option 2
below.
Working Example
from fastapi import FastAPI, APIRouter, Response, Request
from starlette.background import BackgroundTask
from fastapi.routing import APIRoute
from starlette.types import Message
from typing import Dict, Any
import logging
app = FastAPI()
logging.basicConfig(filename='info.log', level=logging.DEBUG)
def log_info(req_body, res_body):
logging.info(req_body)
logging.info(res_body)
async def set_body(request: Request, body: bytes):
async def receive() -> Message:
return {'type': 'http.request', 'body': body}
request._receive = receive
#app.middleware('http')
async def some_middleware(request: Request, call_next):
req_body = await request.body()
await set_body(request, req_body)
response = await call_next(request)
res_body = b''
async for chunk in response.body_iterator:
res_body += chunk
task = BackgroundTask(log_info, req_body, res_body)
return Response(content=res_body, status_code=response.status_code,
headers=dict(response.headers), media_type=response.media_type, background=task)
#app.post('/')
def main(payload: Dict[Any, Any]):
return payload
In case you would like to perform some validation on the request body—for example, ensruing that the request body size is not exceeding a certain value—instead of using request.body(), you can process the body one chunk at a time using the .stream() method, as shown below (similar to this answer).
#app.middleware('http')
async def some_middleware(request: Request, call_next):
req_body = b''
async for chunk in request.stream():
req_body += chunk
...
Option 2 - Using custom APIRoute class
You can alternatively use a custom APIRoute class—similar to here and here—which, among other things, would allow you to manipulate the request body before it is processed by your application, as well as the response body before it is returned to the client. This option also allows you to limit the usage of this class to the routes you wish, as only the endpoints under the APIRouter (i.e., router in the example below) will use the custom APIRoute class .
It should be noted that the same comments mentioned in Option 1 above, under the "Note" section, apply to this option as well. For example, if your API returns a StreamingResponse—such as in /video route of the example below, which is streaming a video file from an online source (public videos to test this can be found here, and you can even use a longer video than the one used below to see the effect more clearly)—you may come across issues on server side, if your server's RAM can't handle it, as well as delays on client side (and reverse proxy server, if using one) due to the whole (streaming) response being read and stored in RAM, before it is returned to the client (as explained earlier). In such cases, you could exclude such endpoints that return a StreamingResponse from the custom APIRoute class and limit its usage only to the desired routes—especially, if it is a large video file, or even live video that wouldn't likely make much sense to have it stored in the logs—simply by not using the #<name_of_router> decorator (i.e., #router in the example below) for such endpoints, but rather using the #<name_of_app> decorator (i.e., #app in the example below), or some other APIRouter or sub application.
Working Example
from fastapi import FastAPI, APIRouter, Response, Request
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse
from fastapi.routing import APIRoute
from starlette.types import Message
from typing import Callable, Dict, Any
import logging
import httpx
def log_info(req_body, res_body):
logging.info(req_body)
logging.info(res_body)
class LoggingRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
req_body = await request.body()
response = await original_route_handler(request)
if isinstance(response, StreamingResponse):
res_body = b''
async for item in response.body_iterator:
res_body += item
task = BackgroundTask(log_info, req_body, res_body)
return Response(content=res_body, status_code=response.status_code,
headers=dict(response.headers), media_type=response.media_type, background=task)
else:
res_body = response.body
response.background = BackgroundTask(log_info, req_body, res_body)
return response
return custom_route_handler
app = FastAPI()
router = APIRouter(route_class=LoggingRoute)
logging.basicConfig(filename='info.log', level=logging.DEBUG)
#router.post('/')
def main(payload: Dict[Any, Any]):
return payload
#router.get('/video')
def get_video():
url = 'https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4'
def gen():
with httpx.stream('GET', url) as r:
for chunk in r.iter_raw():
yield chunk
return StreamingResponse(gen(), media_type='video/mp4')
app.include_router(router)
You may try to customize APIRouter like in FastAPI official documentation:
import time
from typing import Callable
from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute
class TimedRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
before = time.time()
response: Response = await original_route_handler(request)
duration = time.time() - before
response.headers["X-Response-Time"] = str(duration)
print(f"route duration: {duration}")
print(f"route response: {response}")
print(f"route response headers: {response.headers}")
return response
return custom_route_handler
app = FastAPI()
router = APIRouter(route_class=TimedRoute)
#app.get("/")
async def not_timed():
return {"message": "Not timed"}
#router.get("/timed")
async def timed():
return {"message": "It's the time of my life"}
app.include_router(router)
As the other answers did not work for me and I searched quite extensively on stackoverflow to fix this problem, I will show my solution below.
The main issue is that when using the request body or response body many of the approaches/solutions offered online do simply not work as the request/response body is consumed in reading it from the stream.
To solve this issue I adapted an approach that basically reconstructs the request and response after reading them. This is heavily based on the comment by user 'kovalevvlad' on https://github.com/encode/starlette/issues/495.
Custom middleware is created that is later added to the app to log all requests and responses. Note that you need some kind of logger to store your logs.
from json import JSONDecodeError
import json
import logging
from typing import Callable, Awaitable, Tuple, Dict, List
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response, StreamingResponse
from starlette.types import Scope, Message
# Set up your custom logger here
logger = ""
class RequestWithBody(Request):
"""Creation of new request with body"""
def __init__(self, scope: Scope, body: bytes) -> None:
super().__init__(scope, self._receive)
self._body = body
self._body_returned = False
async def _receive(self) -> Message:
if self._body_returned:
return {"type": "http.disconnect"}
else:
self._body_returned = True
return {"type": "http.request", "body": self._body, "more_body": False}
class CustomLoggingMiddleware(BaseHTTPMiddleware):
"""
Use of custom middleware since reading the request body and the response consumes the bytestream.
Hence this approach to basically generate a new request/response when we read the attributes for logging.
"""
async def dispatch( # type: ignore
self, request: Request, call_next: Callable[[Request], Awaitable[StreamingResponse]]
) -> Response:
# Store request body in a variable and generate new request as it is consumed.
request_body_bytes = await request.body()
request_with_body = RequestWithBody(request.scope, request_body_bytes)
# Store response body in a variable and generate new response as it is consumed.
response = await call_next(request_with_body)
response_content_bytes, response_headers, response_status = await self._get_response_params(response)
# Logging
# If there is no request body handle exception, otherwise convert bytes to JSON.
try:
req_body = json.loads(request_body_bytes)
except JSONDecodeError:
req_body = ""
# Logging of relevant variables.
logger.info(
f"{request.method} request to {request.url} metadata\n"
f"\tStatus_code: {response.status_code}\n"
f"\tRequest_Body: {req_body}\n"
)
# Finally, return the newly instantiated response values
return Response(response_content_bytes, response_status, response_headers)
async def _get_response_params(self, response: StreamingResponse) -> Tuple[bytes, Dict[str, str], int]:
"""Getting the response parameters of a response and create a new response."""
response_byte_chunks: List[bytes] = []
response_status: List[int] = []
response_headers: List[Dict[str, str]] = []
async def send(message: Message) -> None:
if message["type"] == "http.response.start":
response_status.append(message["status"])
response_headers.append({k.decode("utf8"): v.decode("utf8") for k, v in message["headers"]})
else:
response_byte_chunks.append(message["body"])
await response.stream_response(send)
content = b"".join(response_byte_chunks)
return content, response_headers[0], response_status[0]

How to get the cookies from an HTTP request using FastAPI?

Is it possible to get the cookies when someone hits the API? I need to read the cookies for each request.
#app.get("/")
async def root(text: str, sessionKey: str = Header(None)):
print(sessionKey)
return {"message": text+" returned"}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=5001 ,reload=True)
You can do it in the same way you are accessing the headers in your example (see docs):
from fastapi import Cookie
#app.get("/")
async def root(text: str, sessionKey: str = Header(None), cookie_param: int | None = Cookie(None)):
print(cookie_param)
return {"message": f"{text} returned"}
Option 1
Use the Request object to get the cookie you wish, as described in Starlette documentation.
from fastapi import Request
#app.get('/')
async def root(request: Request):
print(request.cookies.get('sessionKey'))
return 'OK'
Option 2
Use the Cookie parameter, as described in FastAPI documentation.
from fastapi import Cookie
#app.get('/')
async def root(sessionKey: str = Cookie(None)):
print(sessionKey)
return 'OK'

Unit Testing Replace remote API Server with predefined response

So, I have a server running FastAPI which will make a API call to a remote API upon request.
I am developping unit-testing for this application, but here comes the question:
Can I, for the purpose of the test, replace a legit remote API server response by a predefined response ?
Example of the tests runned:
from fastapi.testclient import TestClient
from web_api import app
client = TestClient(app)
def test_get_root():
response = client.get('/')
assert response.status_code == 200
assert response.json() == {"running": True}
And the my server
from fastapi import FastAPI
app = FastAPI()
#app.get("/")
def home():
return {"running": True}
This is a simple example, but on other endpoints of my API I would call an external remote API
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
Because I want to test the response of MY API, I would like to replace the remote API with a predefined response.
Also, one user request can end-up in multiple background API requests with transformed pieces of data.
Edit
Here are some more details on the structure of the application:
#app.get("/stuff/.......",
# lots of params
)
def get_stuff_from_things(stuff:list, params):
api = API(api_key=...)
# Do some stuff with the params
things = generate_things_list(params)
api.search_things(params)
# Check the result
# do some other stuff
return some_response
class API:
BASE_URL = 'https://api.example.com/'
def search_things(self, params):
# Do some stuff
# like putting stuff in the params
for s in stuff:
s.update(self.get_thing(params)) # -> get_thing()
# Do some more stuff
return stuff
# get_thing <- search_things
def get_thing(self, params...):
# Some stuff
results = self.call_api('something', params) # -> call_api()
json = results.json()
# Some more stuff
things = []
for thing in json['things']:
t = Thing(thing)
things.append(t)
return things
# call_api <- get_thing
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
self.last_response = response
return response
Nb. That is pseudo-code, I simplified the functions by removing the parameters, etc.
I hope it is clear, thanks for your help.
A complex API method might look like this (please pay attention to the depends mechanism - it is crucial):
import urllib
import requests
from fastapi import FastAPI, Depends
app = FastAPI()
# this can be in a different file
class RemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
url = baseurl + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
#app.get("/complex_api")
def calls_other_api(remote_call_wrapper=Depends(RemoteCallWrapper)):
response = remote_call_wrapper.call_api("https://jsonplaceholder.typicode.com",
"/todos/1", None)
return {"result": response.json()}
Now, we wish to replace the remote call class. I wrote a helper library that simplifies the replacement for tests - pytest-fastapi-deps:
from fastapi.testclient import TestClient
from mock.mock import Mock
from requests import Response
from web_api import app, RemoteCallWrapper
client = TestClient(app)
class MyRemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
the_response = Mock(spec=Response)
the_response.json.return_value = {"my": "response"}
return the_response
def test_get_root(fastapi_dep):
with fastapi_dep(app).override({RemoteCallWrapper: MyRemoteCallWrapper}):
response = client.get('/complex_api')
assert response.status_code == 200
assert response.json() == {"result": {"my": "response"}}
You override the RemoteCallWrapper with your MyRemoteCallWrapper implementation for the test, which has the same spec.
As asserted - the response changed to our predefined response.
It sounds like you'd want to mock your call_api() function.
With a small modification to call_api() (returning the result of .json()), you can easily mock the whole function while calling the endpoint in your tests.
I'll use two files, app.py and test_app.py, to demonstrate how I would do this:
# app.py
import requests
import urllib
from fastapi import FastAPI
app = FastAPI()
def call_api(self, endpoint: str, params: dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response.json() # <-- This is the only change. Makes it easier to test things.
#app.get("/")
def home():
return {"running": True}
#app.get("/call-api")
def make_call_to_external_api():
# `endpoint` and `params` could be anything here and could be different
# depending on the query parameters when calling this endpoint.
response = call_api(endpoint="something", params={})
# Do something with the response...
result = response["some_parameter"]
return result
# test_app.py
from unittest import mock
from fastapi import status
from fastapi.testclient import TestClient
import app as app_module
from app import app
def test_call_api_endpoint():
test_response = {
"some_parameter": "some_value",
"another_parameter": "another_value",
}
# The line below will "replace" the result of `call_api()` with whatever
# is given in `return_value`. The original function is never executed.
with mock.patch.object(app_module, "call_api", return_value=test_response) as mock_call:
with TestClient(app) as client:
res = client.get("/call-api")
assert res.status_code == status.HTTP_200_OK
assert res.json() == "some_value"
# Make sure the function has been called with the right parameters.
# This could be dynamic based on how the endpoint has been called.
mock_call.assert_called_once_with(endpoint="something", params={})
If app.py and test_app.py are in the same directory you can run the tests simply by running pytest inside that directory.

How to customize error schema in FastApi and Pydantic?

I make FastAPI application I face to structural problem.
For example, let's say there is exist this simple application
from fastapi import FastAPI, Header
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class PydanticSchema(BaseModel):
test_int: int = Field(..., ge=0)
class CustomException(Exception):
def __init__(self, status_code, msg):
self.status_code = status_code
self.msg = msg
#app.exception_handler(CustomException)
async def handle_custom_exception(request, exc: CustomException):
return JSONResponse(
status_code=exc.status_code, content={"error": exc.msg}
)
#app.post("/")
def index(
some_form: PydanticSchema,
some_header: str = Header("", alias="X-Header", max_length=3),
):
is_special_case = some_form.test_int == 42
if is_special_case:
raise CustomException(418, "Very special case")
return "ok"
And here is the problem.
I have THREE independent producers of errors: My manual raise, Pydantic Model, FastApi.
if __name__ == "__main__":
client = TestClient(app)
cases = (
({"X-Header": "abc"}, {"test_int": 0}),
({"X-Header": "abcdef"}, {"test_int": 0}),
({"X-Header": "abc"}, {"test_int": -1}),
({"X-Header": "abc"}, {"test_int": 42}),
)
for case in cases:
r = client.post("/", headers=case[0], json=case[1])
print(f"#####\n{case=}:\n{r.status_code=}\n{r.text=}")
My goal is to have consistent error responses
So I have to add exception handler for my CustomException,
I have to add error_msg_templates to PydanticModel.Config
I have to add incredibly stupid handler for FastApi exception and Pydantic exception to reformat them into my custom response. Becasuse fastapi overrides custom messages from pydantic templates.
Like so:
# ---snip---
from fastapi.exceptions import RequestValidationError
# ---snip---
class PydanticSchema(BaseModel):
test_int: int = Field(..., ge=0)
class Config:
error_msg_templates = {"value_error.number.not_ge": "Custom GE error"}
# ---snip---
#app.exception_handler(RequestValidationError)
async def fastapi_error_handler(request, exc: RequestValidationError):
errors = exc.errors()
error_wrapper = exc.raw_errors[0]
validation_error = error_wrapper.exc
from pydantic import error_wrappers as ew
if isinstance(validation_error, ew.ValidationError):
errors = validation_error.errors()
first_error = errors[0]
msg = first_error.get("msg")
# [!] Demonstration
etype = first_error.get("type")
if etype == "value_error.any_str.max_length":
msg = "Custom MAX length error"
return JSONResponse(status_code=400, content={"error": msg})
# ---snip---
It is unmaintainable! But custom error message is required - I can not rely ond default pydantic\fastapi error response schemas.
Does anyone one faced to this problem and have solved it gracefully?
Why not FastAPI's Exception_handler argument? You can view the status code of the response when this error occurs, then add an exception handler. Exception handlers have access to the Request & Exception objects, which are the request that caused the exception, and the exception raised, respectively.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
def val_err(request: Request, exception: Exception):
if not isinstance(exception, RequestValidationError):
return
errors = exc.errors()
error_wrapper = exc.raw_errors[0]
validation_error = error_wrapper.exc
from pydantic import error_wrappers as ew
if isinstance(validation_error, ew.ValidationError):
errors = validation_error.errors()
first_error = errors[0]
msg = first_error.get("msg")
# [!] Demonstration
etype = first_error.get("type")
if etype == "value_error.any_str.max_length":
msg = "Custom MAX length error"
return JSONResponse(status_code=400, content={"error": msg})
exception_handlers = {422: val_err}
app = FastAPI(exception_handlers=exception_handlers)
You can refer to my other answer which dissects the code and explains it in a bit more detail.

FastApi Post Request With Bytes Object Got 422 Error

I am writing a python post request with a bytes body:
with open('srt_file.srt', 'rb') as f:
data = f.read()
res = requests.post(url='http://localhost:8000/api/parse/srt',
data=data,
headers={'Content-Type': 'application/octet-stream'})
And in the server part, I tried to parse the body:
app = FastAPI()
BaseConfig.arbitrary_types_allowed = True
class Data(BaseModel):
data: bytes
#app.post("/api/parse/{format}", response_model=CaptionJson)
async def parse_input(format: str, data: Data) -> CaptionJson:
...
However, I got the 422 error:
{"detail":[{"loc":["body"],"msg":"value is not a valid dict","type":"type_error.dict"}]}
So where is wrong with my code, and how should I fix it?
Thank you all in advance for helping out!!
FastAPI by default will expect you to pass json which will parse into a dict. It can't do that if it's isn't json, which is why you get the error you see.
You can use the Request object instead to receive arbitrary bytes from the POST body.
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/foo")
async def parse_input(request: Request):
data: bytes = await request.body()
# Do something with data
You might consider using Depends which will allow you to clean up your route function. FastAPI will first call your dependency function (parse_body in this example) and will inject that as an argument into your route function.
from fastapi import FastAPI, Request, Depends
app = FastAPI()
async def parse_body(request: Request):
data: bytes = await request.body()
return data
#app.get("/foo")
async def parse_input(data: bytes = Depends(parse_body)):
# Do something with data
pass
If the endgoal of your request is to only send bytes then please look at the documentation of FastAPI to accept bytes-like objects: https://fastapi.tiangolo.com/tutorial/request-files.
There is no need for the bytes to be enclosed into a model.

Categories

Resources