Search code examples
pythonfastapistarlette

fastapi (starlette) RedirectResponse redirect to post instead get method


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

Solution

  • 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