Search code examples
pythonrestfastapihttp-status-code-405

FastAPI GET endpoint returns "405 method not allowed" response


A GET endpoint in FastAPI is returning correct result, but returns 405 method not allowed when curl -I is used. This is happening with all the GET endpoints. As a result, the application is working, but health check on application from a load balancer is failing.

Any suggestions what could be wrong?

code

@app.get('/health')
async def health():
    """
    Returns health status
    """
    return JSONResponse({'status': 'ok'})

result

curl http://172.xx.xx.xx:8080

return header

curl -I http://172.xx.xx.xx:8080


Solution

  • The curl -I option (which is used in the example you provided) is the same as using curl --head and performrs an HTTP HEAD request, in order to fetch the headers only (not the body/content of the resource):

    The HTTP HEAD method requests the headers that would be returned if the HEAD request's URL was instead requested with the HTTP GET method. For example, if a URL might produce a large download, a HEAD request could read its Content-Length header to check the filesize without actually downloading the file.

    The requested resource/endpoint you are trying to call supports only GET requests; hence, the 405 Method Not Allowed response status code, which indicates that the server knows the request method, but the target resource doesn't support this method.

    To demonstrate this, have a look at the example below:

    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get('/')
    async def main():
        return {'Hello': 'World'}
    

    Test using Python requests (similar result is obtained using curl -I http://127.0.0.1:8000)

    import requests
    
    # Making a GET request
    # r = requests.get('http://127.0.0.1:8000')
      
    # Making a HEAD request
    r = requests.head('http://127.0.0.1:8000')
      
    # check status code for response received
    print(r.status_code, r.reason)
      
    # print headers of request
    print(r.headers)
      
    # checking if request contains any content
    print(r.content)
    

    Output (indicating by the allow response header which request methods are supported by the requested resource):

    405 Method Not Allowed
    {'date': 'Sun, 12 Mar 2023', 'server': 'uvicorn', 'allow': 'GET', 'content-length': '31', 'content-type': 'application/json'}
    b''
    

    If, instead, you performed a GET request (in order to issue a GET request in the example above, uncomment the line for GET request and comment the one for HEAD request, or in curl use curl http://127.0.0.1:8000), the response would be as follows:

    200 OK
    {'date': 'Sun, 12 Mar 2023', 'server': 'uvicorn', 'content-length': '17', 'content-type': 'application/json'}
    b'{"Hello":"World"}'
    

    Solutions

    To make a FastAPI endpoint supporting more than one HTTP request methods (e.g., both GET and HEAD requests), the following solutions are available.

    Solution 1

    Cretate two different functions with a decorator refering to the request method for which they should be called, i.e., @app.head() and @app.get(), or, if the logic of the function is the same for both the request methods, then simply add a decorator for each request method that you would like the endpoint to support to the same function. For instance:

    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.head('/')
    @app.get('/')
    async def main():
        return {'msg': 'Hello World'}
    

    Solution 2

    Use the @app.api_route() decorator, which allows you to define the set of supported request methods for the endpoint. For example:

    from fastapi import FastAPI
    
    app = FastAPI()
     
    @app.api_route('/', methods=['GET', 'HEAD'])
    async def main():
        return {'msg': 'Hello World'}
    

    Output

    Both solutions above would respond as follows (when a HEAD request is issued by a client):

    200 OK
    {'date': 'Sun, 12 Mar 2023', 'server': 'uvicorn', 'content-length': '17', 'content-type': 'application/json'}
    b''
    

    In both solutions, if you would like to distinguish between the methods used to call the endpoint, you could use the .method attribute of the Request object. For instance:

    from fastapi import FastAPI, Request
    
    app = FastAPI()
     
    @app.api_route('/', methods=['GET', 'HEAD'])
    async def main(request: Request):
        if request.method == 'GET':
            print('GET method was used')
        elif request.method == 'HEAD':
            print('HEAD method was used')
        
        return {'msg': 'Hello World'}
    

    Note 1: In the test example using Python requests above, please avoid calling the json() method on the response when performing a HEAD request, as a response to a HEAD method request does not normally have a body, and it would raise an exception, if attempted calling r.json()—see this answer for more details. Instead, use print(r.content), as demonstrated in the example above on HEAD requests, if you would like to check whether or not there is a response body.

    Note 2: The 405 Method Not Allowed response may also be caused by other reasons—see related answers here and here, as well as here and here.