fastapi (starlette) RedirectResponse redirect to post instead get method

Question:

I have encountered strange redirect behaviour after returning a RedirectResponse object

events.py

router = APIRouter()

@router.post('/create', response_model=EventBase)
async def event_create(
        request: Request,
        user_id: str = Depends(get_current_user),
        service: EventsService = Depends(),
        form: EventForm = Depends(EventForm.as_form)
):
    event = await service.post(
       ...
   )
    redirect_url = request.url_for('get_event', **{'pk': event['id']})
    return RedirectResponse(redirect_url)


@router.get('/{pk}', response_model=EventSingle)
async def get_event(
        request: Request,
        pk: int,
        service: EventsService = Depends()
):
    ....some logic....
    return templates.TemplateResponse(
        'event.html',
        context=
        {
            ...
        }
    )

routers.py

api_router = APIRouter()

...
api_router.include_router(events.router, prefix="/event")

this code returns the result

127.0.0.1:37772 - "POST /event/22 HTTP/1.1" 405 Method Not Allowed

OK, I see that for some reason a POST request is called instead of a GET request. I search for an explanation and find that the RedirectResponse object defaults to code 307 and calls POST link

I follow the advice and add a status

redirect_url = request.url_for('get_event', **{'pk': event['id']}, status_code=status.HTTP_302_FOUND)

And get

starlette.routing.NoMatchFound

for the experiment, I’m changing @router.get('/{pk}', response_model=EventSingle) to @router.post('/{pk}', response_model=EventSingle)

and the redirect completes successfully, but the post request doesn’t suit me here. What am I doing wrong?

UPD

html form for running event/create logic

base.html

<form action="{{ url_for('event_create')}}" method="POST">
...
</form>

base_view.py

@router.get('/', response_class=HTMLResponse)
async def main_page(request: Request,
                    activity_service: ActivityService = Depends()):
    activity = await activity_service.get()
    return templates.TemplateResponse('base.html', context={'request': request,
                                                            'activities': activity})
Asked By: Jekson

||

Answers:

When you want to redirect to a GET after a POST, the best practice is to redirect with a 303 status code, so just update your code to:

    # ...
    return RedirectResponse(redirect_url, status_code=303)

As you’ve noticed, redirecting with 307 keeps the HTTP method and body.

Fully working example:

from fastapi import FastAPI, APIRouter, Request
from fastapi.responses import RedirectResponse, HTMLResponse


router = APIRouter()

@router.get('/form')
def form():
    return HTMLResponse("""
    <html>
    <form action="/event/create" method="POST">
    <button>Send request</button>
    </form>
    </html>
    """)

@router.post('/create')
async def event_create(
        request: Request
):
    event = {"id": 123}
    redirect_url = request.url_for('get_event', **{'pk': event['id']})
    return RedirectResponse(redirect_url, status_code=303)


@router.get('/{pk}')
async def get_event(
        request: Request,
        pk: int,
):
    return f'<html>oi pk={pk}</html>'

app = FastAPI(title='Test API')

app.include_router(router, prefix="/event")

To run, install pip install fastapi uvicorn and run with:

uvicorn --reload --host 0.0.0.0 --port 3000 example:app

Then, point your browser to: http://localhost:3000/event/form

Answered By: Elias Dorneles

The error you mention here is raised because you are trying to access the event_create endpoint via http://127.0.0.1:8000/event/create, for instance. However, since event_create route handles POST requests, your request ends up in the get_event endpoint (and raises a value is not a valid integer error, since you are passing a string instead of integer), as when you type a URL in the address bar of your browser, it performs a GET request.

Thus, you need an HTML <form>, for example, to submit a POST request to the event_create endpoint. Below is a working example, which you can use to access the HTML <form> at http://127.0.0.1:8000/event/ (adjust the port number as desired) to send a POST request, which will then trigger the RedirectResponse.

As @tiangolo mentioned here, when performing a RedirectResponse from a POST request route to a GET request route, the response status code has to change to 303 See Other. For instance:

return RedirectResponse(redirect_url, status_code=status.HTTP_303_SEE_OTHER) 

Working Example:

from fastapi import APIRouter, FastAPI, Request, status
from fastapi.responses import RedirectResponse, HTMLResponse

router = APIRouter()

# This endpoint can be accessed at http://127.0.0.1:8000/event/
@router.get('/', response_class=HTMLResponse)
def event_create_form(request: Request):
    return """
    <html>
       <body>
          <h1>Create an event</h1>
          <form method="POST" action="/event/create">
             <input type="submit" value="Create Event">
          </form>
       </body>
    </html>
    """
    
@router.post('/create')
def event_create(request: Request):
    event = {"id": 1}
    redirect_url = request.url_for('get_event', **{'pk': event['id']})
    return RedirectResponse(redirect_url, status_code=status.HTTP_303_SEE_OTHER)    

@router.get('/{pk}')
def get_event(request: Request, pk: int):
    return {"pk": pk}


app = FastAPI()
app.include_router(router, prefix="/event")
Answered By: Chris
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.