9

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})
Jekson
  • 2,892
  • 8
  • 44
  • 79
  • 1
    Please have a look [here](https://stackoverflow.com/a/70693108/17865804) if it helps. – Chris Jan 19 '22 at 16:21
  • 1
    with `status_code=status.HTTP_303_SEE_OTHER` same result `starlette.routing.NoMatchFound` – Jekson Jan 19 '22 at 16:31
  • 1
    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? – Jekson Jan 19 '22 at 16:42
  • 1
    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. – Jekson Jan 20 '22 at 08:46
  • 1
    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 ? – Jekson Jan 20 '22 at 08:48
  • @Jekson 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. – Elias Dorneles Jan 20 '22 at 09:07
  • @EliasDorneles that's the point! I was very inattentive, incorrect syntax was the cause of the problem. – Jekson Jan 20 '22 at 09:16
  • You're welcome, have a nice day! :) – Elias Dorneles Jan 20 '22 at 10:18

2 Answers2

11

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

Elias Dorneles
  • 22,556
  • 11
  • 85
  • 107
  • with `status_code=303` same result `starlette.routing.NoMatchFound` – Jekson Jan 19 '22 at 16:31
  • Well, your problem is probably elsewhere. Here is a working example: https://gist.github.com/eliasdorneles/6b2afd81cfc15ad4084d3da620bef73f (instructions how to run in the comments) – Elias Dorneles Jan 19 '22 at 17:05
  • 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 above – Jekson Jan 19 '22 at 17:26
  • 1
    @Jekson ah sorry, i had made a mistake in the instructions, you need to point your browser to http://localhost:3000/event/form -- and not http://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) – Elias Dorneles Jan 19 '22 at 20:16
  • @Jekson 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... – Elias Dorneles Jan 19 '22 at 20:19
  • This way `localhost:3000/event/form` al work correct – Jekson Jan 20 '22 at 08:52
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")
Chris
  • 18,724
  • 6
  • 46
  • 80