fastapi (starlette) RedirectResponse redirect to post instead get method
Asked Answered
S

2

12

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})
Sharleensharlene answered 19/1, 2022 at 16:4 Comment(8)
Please have a look here if it helps.Pliske
with status_code=status.HTTP_303_SEE_OTHER same result starlette.routing.NoMatchFoundSharleensharlene
I should add that a standard html button form is used to run the code, but I don't think it matters. What other information might be useful?Sharleensharlene
I definitely don't understand from the suggested answers how I can make my code work. I'm running the logic to create an event via an html form, just like in the answers. I've added it to the question description.Sharleensharlene
Or are you telling me that the logic in my code is correct and I need to look for the problem somewhere else rather than RedirectResponse ?Sharleensharlene
@Sharleensharlene One difference from your code to my example is that you're passing the status_code to url_for instead of adding it to the RedirectResponse, that's probably why you're getting the NoMatchFound: it'strying to match a route with a parameter status_code and not finding it.Varini
@EliasDorneles that's the point! I was very inattentive, incorrect syntax was the cause of the problem.Sharleensharlene
You're welcome, have a nice day! :)Varini
V
17

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

Varini answered 19/1, 2022 at 16:23 Comment(6)
with status_code=303 same result starlette.routing.NoMatchFoundSharleensharlene
Well, your problem is probably elsewhere. Here is a working example: gist.github.com/eliasdorneles/6b2afd81cfc15ad4084d3da620bef73f (instructions how to run in the comments)Varini
I try and got {"detail":[{"loc":["path","pk"],"msg":"value is not a valid integer","type":"type_error.integer"}]} . is that what you mean? But my code works if I change the get to post in the rout as I wrote aboveSharleensharlene
@Sharleensharlene ah sorry, i had made a mistake in the instructions, you need to point your browser to localhost:3000/event/form -- and not localhost:3000/event/create (which will do a GET request that will fail because it will try to match against /event/{pk}, that's the error you saw)Varini
@Sharleensharlene and indeed, the redirect in your code will work if you change the GET /event/{pk} into POST /event/{pk} -- but is that a good idea? IMO, it would be hurting your API design, just because of an annoyance of the framework...Varini
This way localhost:3000/event/form al work correctSharleensharlene
P
3

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")
Pliske answered 19/1, 2022 at 20:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.