Fastapi: [QUESTION] How to send 204 response?

Created on 19 Nov 2019  路  17Comments  路  Source: tiangolo/fastapi

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.

question

Most helpful comment

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.

All 17 comments

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).

It is a bug. I found that if (event.status_code in (204, or request_method == b" or (request_method == b" and 200 <= event.status_code < 300)) than content length set to 0, but by defualt if return None then framevork try to conver None to json and got (null(len 4)). Need to add check on status code and try to convert only if the expression above is False.

@heckad
Could you share where this check is performed in the code?

I don't necessarily think it's a bug for a JSONResponse to convert None to "null", regardless of the status code you return; it's up to you to specify that the response class should be Response and not JSONResponse. But it definitely is a bug if it manually sets the content length to 0 even when that's not the case.

I personally think the most consistent solution would be to drop the special case for the 204 status code (at least for JSONResponse).

It could make sense to display some kind of warning if using a 204 response code with a JSONResponse though?

Could you share where this check is performed in the code?

_body_framing method in _connection in h11 file.

I don't _necessarily_ think it's a bug for a JSONResponse to convert None to "null", regardless of the status code you return; it's up to you to specify that the response class should be Response and not JSONResponse. But it definitely is a bug if it manually sets the content length to 0 even when that's not the case.

I wish he didn't put anything into the body. Add JSONResponse not remove the body and not solve the problem.

I have poor English and I don鈥檛 attack, I just speak briefly.

I meant this check:

(event.status_code in (204, or request_method == b" or (request_method == b" and 200 <= event.status_code < 300)) (which I assumed was in fastapi or starlette).

Is that in h11?

Yes, copy it from h11 file.

Stacktrace:

Traceback (most recent call last):
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 385, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\fastapi\applications.py", line 139, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\applications.py", line 134, in __call__
    await self.error_middleware(scope, receive, send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\middleware\errors.py", line 178, in __call__
    raise exc from None
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\middleware\errors.py", line 156, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\exceptions.py", line 73, in __call__
    raise exc from None
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\exceptions.py", line 62, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\routing.py", line 590, in __call__
    await route(scope, receive, send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\routing.py", line 208, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\routing.py", line 44, in app
    await response(scope, receive, send)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\responses.py", line 128, in __call__
    await send({"type": "http.response.body", "body": self.body})
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\exceptions.py", line 59, in sender
    await send(message)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\starlette\middleware\errors.py", line 153, in _send
    await send(message)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 479, in send
    output = self.conn.send(event)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\h11\_connection.py", line 464, in send
    data_list = self.send_with_data_passthrough(event)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\h11\_connection.py", line 498, in send_with_data_passthrough
    writer(event, data_list.append)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\h11\_writers.py", line 69, in __call__
    self.send_data(event.data, write)
  File "C:\Users\Asus\.virtualenvs\order_crud-5YVFcgZR\lib\site-packages\h11\_writers.py", line 88, in send_data
    raise LocalProtocolError(
h11._util.LocalProtocolError: Too much data for declared Content-Length

Huh, well that could be a good reason to change the behavior when content-type is 204 regardless of what feels best in FastAPI. Or maybe it makes sense to request a change to the upstream behavior; seems weird that it would override the content length depending on the status code when it is going to check the actual length elsewhere, and potentially raise an error...

So what is the best way to handle this right now? I have a function that returns 204 when results are not ready (but are in progress), and then a regular 200 when its all OK (with content).

I tried to achieve this with:

@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, response: Response):
    # ... snipped ...
    if not complete:
        response.status_code = HTTP_204_NO_CONTENT
        return None

and I run into the same error as posted. Is there a workaround?

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.

Yep. Just what @dmontagu said :point_up:

Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.

@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

@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).

using status_code=HTTP_204_NO_CONTENT I had the same problem but only on a Windows machine:

File "[...]\lib\site-packages\h11\_writers.py", line 89, in send_data
    "Too much data for declared Content-Length"

Running the same code on OSX did not return an error.
Any idea why?

using status_code=HTTP_204_NO_CONTENT I had the same problem but only on a Windows machine:

File "[...]\lib\site-packages\h11\_writers.py", line 89, in send_data
    "Too much data for declared Content-Length"

Running the same code on OSX did not return an error.
Any idea why?

I have the same problem:
File "...libsite-packagesh11_writers.py", line 97, in send_data
raise LocalProtocolError("Too much data for declared Content-Length")

fastapi==0.55.1

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?

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.

Was this page helpful?
0 / 5 - 0 ratings