fastapi: How to send 204 response?

I tried to send 204 response in delete method

Example handler

@router.delete('/{order_id}/', tags=['smart order'], status_code=204)
async def cancel_smart_order(
        session: Session = Depends(get_session),
        order_id: UUID = Path(...)
):
    order = await session.get(order_id)

    if order.status != OrderStatus.open:
        raise HTTPException(409, f'Order have status: {order.status}')

    order.status = OrderStatus.canceled

    await session.commit_only(order)

But got error h11._util.LocalProtocolError: Too much data for declared Content-Length. Seems framefork convert None to null but set content length 0.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 27 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Instead of returning None, and instead of injecting the response, just return a newly created response. It would look like:

@api.get(
    "/test_request_results/{test_request_id}",
    response_model=schemas.TestRequestResults,
    responses={204: {"model": None}},
)
async def read_test_request_results(test_request_id: int):
    # ... snipped ...
    if not complete:
        return Response(status_code=HTTP_204_NO_CONTENT)

The reason you are getting content is because FastAPI uses a JSONResponse by default (instead of a Response), which converts the returned value None to "null". By returning a Response directly, you prevent FastAPI from using the JSONResponse and encoding the None.

This is a bug: FastAPI is not conforming to the HTTP RFC: https://tools.ietf.org/html/rfc2616#section-10.2.5

The 204 response MUST NOT include a message-body, and thus is always terminated by the first empty line after the header fields.

FastAPI should made it difficult to produce an invalid HTTP response, not allowing to create a valid one with a workaround.

Dunno why, but today I ran into the same issue with python3.9. fastapi==0.63.0 and uvicorn==0.13.3, h11==0.12.0 on Debian10:

app_1  | INFO:     172.31.0.1:52908 - "DELETE /api/v1/users/user/2/ HTTP/1.1" 204 No Content
app_1  | ERROR:    Exception in ASGI application
app_1  | Traceback (most recent call last):
app_1  |   File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 394, in run_asgi
app_1  |     result = await app(self.scope, self.receive, self.send)
app_1  |   File "/usr/local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
app_1  |     return await self.app(scope, receive, send)
app_1  |   File "/usr/local/lib/python3.9/site-packages/fastapi/applications.py", line 199, in __call__      
app_1  |     await super().__call__(scope, receive, send)
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/applications.py", line 111, in __call__    
app_1  |     await self.middleware_stack(scope, receive, send)
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 181, in __call__
app_1  |     raise exc from None
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 159, in __call__
app_1  |     await self.app(scope, receive, _send)
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/cors.py", line 86, in __call__  
app_1  |     await self.simple_response(scope, receive, send, request_headers=headers)
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/cors.py", line 142, in simple_response
app_1  |     await self.app(scope, receive, send)
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/base.py", line 26, in __call__  
app_1  |     await response(scope, receive, send)
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/responses.py", line 228, in __call__       
app_1  |     await run_until_first_complete(
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/concurrency.py", line 18, in run_until_first_complete
app_1  |     [task.result() for task in done]
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/concurrency.py", line 18, in <listcomp>    
app_1  |     [task.result() for task in done]
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/responses.py", line 223, in stream_response
app_1  |     await send({"type": "http.response.body", "body": chunk, "more_body": True})
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/cors.py", line 148, in send     
app_1  |     await send(message)
app_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 156, in _send  
app_1  |     await send(message)
app_1  |   File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 487, in send
app_1  |     output = self.conn.send(event)
app_1  |   File "/usr/local/lib/python3.9/site-packages/h11/_connection.py", line 468, in send
app_1  |     data_list = self.send_with_data_passthrough(event)
app_1  |   File "/usr/local/lib/python3.9/site-packages/h11/_connection.py", line 501, in send_with_data_passthrough
app_1  |     writer(event, data_list.append)
app_1  |   File "/usr/local/lib/python3.9/site-packages/h11/_writers.py", line 58, in __call__
app_1  |     self.send_data(event.data, write)
app_1  |   File "/usr/local/lib/python3.9/site-packages/h11/_writers.py", line 78, in send_data
app_1  |     raise LocalProtocolError("Too much data for declared Content-Length")
app_1  | h11._util.LocalProtocolError: Too much data for declared Content-Length

This helped, just use: response_class=Response

example:

from fastapi import Path, Response

...

@router_v1.delete('/user/{id}/', status_code=204, response_class=Response)
async def delete_user(id: int = Path(..., gt=0)) -> None:
    await UserService.delete(id)

...

Just to mention, I had no issues previously on versions fastapi==0.54.1, uvicorn==0.11.3, h11==0.9.0 (Ubuntu 20.04, python3.8)

the easiest way:

        response = JSONResponse()
        response.status_code = 204
        response.body = b''
        return response

@robmoore and anyone who sees this in the future.

I think the issue you were having is one I also ran into. I was apparently importing this from starlette.responses import Response instead of from fastapi.responses import Response

This solved my issue, anyways.

the easiest way:

I think the best way to handle this is @georgealar solution: (aka setting response_class=Response in the decorator)

@router_v1.delete('/user/{id}/', status_code=204, response_class=Response)
async def delete_user(id: int = Path(..., gt=0)) -> None:
    await UserService.delete(id)

Hi @dmontagu and @tiangolo

I believe FastAPI can do the right thing here with regards to returning empty body when handler declares its status_code as 204. It’s not a trivial to spot and is a potential time sink. Speaking from the fresh experience - I had a code that did:

@app.delete("/", status_code=HTTPStatus.NO_CONTENT)
    async def die():
    # some work
    return

It works fine locally, e.g. when browsing localhost with Chrome or FF. Then I deploy to Google Cloud Run and start getting HTTP 503 Service Unavaialable. Cloud Run is relatively new, so I turn to GCP support and back&forth several hours later I land here to join the club of other devs bit by the same issue.

Too bad, what do you think?

The current full fix is as follows:

from http import HTTPStatus
from fastapi import FastAPI, Response

app = FastAPI()


@app.delete("/", status_code=HTTPStatus.NO_CONTENT)
async def die():
    # some work
    return Response(status_code=HTTPStatus.NO_CONTENT.value)

But even that is not without a quirk - the handler declaration accepts HTTPStatus enum as is, but Response doesn’t resulting in weird HTTP logs like

INFO:     127.0.0.1:49710 - "DELETE / HTTP/1.1" HTTPStatus.NO_CONTENT No Content

Again, too many details to dig into for something I would expect to work out of the box (by high FastAPI standards). Opinions?

@tiangolo I’ve just been reported this bug by mobile devs: HTTP FAILED: java.net.ProtocolException: HTTP 204 had non-zero Content-Length: 4

I believe FastAPI should handle this and set response to Response class if response_code is set to 204

Yep. Just what @dmontagu said ☝️

Not sure why the content-length is wrong; that may be a bug. But if you want to return an actual empty response, just set response_class=Response in the route decorator (the default is JSONResponse, which converts None to "null" as you have noticed).

@tiangolo It does seem that a lot of lower-level frameworks/tools are explicitly checking for 204s to be empty, and raising errors when they aren’t. The way things are implemented now, it admittedly feels like it would be a little unnatural to automatically modify the response class for just a single value of the return code, but given the extent to which this is special-cased in other tools, maybe it makes sense.

Either way, it probably makes to at least make a note of this behavior in the docs (probably somewhere near this section, which does explicitly discuss the 204 response code: https://fastapi.tiangolo.com/tutorial/response-status-code/#about-http-status-codes).