FastAPI: How to redirect from a POST endpoint to a GET endpoint carrying the data submitted using the POST request?

380 views Asked by At

I have a problem: I can’t understand how I can pass additional data to the redirect

I read the answers: this, this, this but these links did not give me clarity

in HTML file script:

function cmb_pos() {
        var data = {
            'dep_id': dep_id, 'div_id': div_id,
        }
        fetch('/job-history/add-new/intermediate/{{persona_id}}', {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(data)
        })
            .then(response => response.json())
            .then(response => console.log(JSON.stringify(response)))
    };

first: (a function that accepts a POST, performs actions and redirects to another function)

from starlette.datastructures import URL

@router.post(path="/add-new/intermediate/{persona_id}",
    response_class=responses.RedirectResponse,
)
async def my_test(request: Request, item: Item, persona_id: int, service: Related):
    staff_schedule = await service.get_item_by_where(....)
    context_ext = {}
    context_ext["request"] = request
    context_ext["staff_schedule"] = staff_schedule
    context_ext["flag"] = True

    redirect_url = URL(request.url_for("create_job_history_init", persona_id=persona_id))
                  .include_query_params(context_ext=context_ext)
    return responses.RedirectResponse(
        url=redirect_url,
        status_code=status.HTTP_303_SEE_OTHER,
    )

second: (function to which the redirect occurs)

@router.get(path="/persona_id={persona_id}/add-new",
    response_class=responses.HTMLResponse,
)
async def create_job_history_init(request: Request, persona_id: int,
    schedule_service: CommonsDep, context_ext: Optional[dict] = None,
):
    context = {}
    schedule = await schedule_service.get_all_item()
    related = ......
    context = schedule | related
    context["request"] = request
    context["persona_id"] = persona_id
    context["context_ext"] = context_ext

    return backend.webapps.templates.TemplateResponse(
        name="job_history/create_job_history.html",
        context=context,
    )

I got this error:

File "/Users/pas/------/backend/webapps/job_history.py", line 276, in my_test ).include_query_params(context_ext=context_ext)

File "/Users/pas/-------/.my_pers/lib/python3.11/site-packages/starlette/datastructures.py", line 143, in include_query_params params = MultiDict(parse_qsl(self.query, keep_blank_values=True))

File "/Users/pas/---------/.my_pers/lib/python3.11/site-packages/starlette/datastructures.py", line 83, in query return self.components.query

File "/Users/pas/---------/.my_pers/lib/python3.11/site-packages/starlette/datastructures.py", line 66, in components self._components = urlsplit(self._url)

TypeError: unhashable type: 'URL'

what am I doing wrong? Is it possible to transfer additional data via a redirect?

thank you very much in advance

update: in this This solution is proposed, but I get an error in it

return tuple(x.decode(encoding, errors) if x else '' for x in args)

AttributeError: 'URL' object has no attribute 'decode'

parsed = list(urllib.parse.urlparse(redirect_url))

for me the right solution is this:

parsed = list(urllib.parse.urlparse(redirect_url ._url )) after adding ._url I was able to parse

2

There are 2 answers

2
Chris On

Current Approach

You are using JavaScript Fetch API to make a POST request with JSON data to the ceate_job_history_intermediate() endpoint of your API. Next, that endpoint responds with a RedirectResponse to a GET endpoint, after converting the JSON (body) data into query parameters and adding them to the URL (Note: when redirecting from a POST endpoint to a GET endpoint, the response status code has to change to 303 or 302 or 301. Please take a look at the following posts for more details and examples: here, here, as well as here, here and here). However, as you are using a JavaScript fetch() request, the browser can't redirect you to the new webpage, unless you follow one of the approaches described in this answer.

Issues with Current Approach

In HTTP, a redirect response with status codes that fall under the 3xx category, as mentioned above, indicate that a requested resource has been moved to a new location, and the browser should redirect the request to the new location. Hence, when returning a RedirectResponse from the ceate_job_history_intermediate() endpoint (which, as you may soon realise, is not needed at all), this response actually goes back to the browser first, which is instructed to issue a GET request with the data (that you submitted earlier) now being part of the query string. As you may understand, this approach makes little sense. You might as well send the data as query parameters in the first place, or, preferably, as Form data in the request body, as passing sensitive data in the query string poses serious security/privacy risks—have a look at the top of this answer for more details on that subject. The following solutions implement the aforementioned approaches, using an HTML <form> instead of JavaScript Fetch API. However, in the case of passing the data as part of the query string, you might as well use a fetch() request instead (see this answer and the following links). Please have a look at these answers from which the below approaches are derived: this and this (you might find this useful as well).

Solution 1 - Passing Form data in the request body

app.py (defining Form parameters directly in the endpoint)

from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates


app = FastAPI()
templates = Jinja2Templates(directory='templates')

    
@app.post('/submit')
def get_result(request: Request, msg: str = Form(...)):
    context = {'request': request, 'msg': msg}
    return templates.TemplateResponse('result.html', context)


@app.get('/')
def index(request: Request):
    return templates.TemplateResponse('index.html', {'request': request})

In case you had multiple Form parameters and wanted to define them in a class instead of the endpoint, you could use a @dataclass, as shown below (see this answer for more details):

app.py (defining Form parameters using a @dataclass)

from fastapi import FastAPI, Request, Form, Depends
from fastapi.templating import Jinja2Templates
from dataclasses import dataclass

app = FastAPI()
templates = Jinja2Templates(directory='templates')
 

@dataclass
class Item:
    msg: str = Form(...)
    
    
@app.post('/submit')
def get_result(request: Request, item: Item = Depends()):
    context = {'request': request, 'msg': item.msg}
    return templates.TemplateResponse('result.html', context)

    
@app.get('/')
def index(request: Request):
    return templates.TemplateResponse('index.html', {'request': request})

templates/index.html

<!DOCTYPE html>
<html>
   <body>
      <h1>First Page</h1>
      <form method="post" action="/submit"  enctype="multipart/form-data">
         Message : <input type="text" name="msg" value="foo"><br>
         <input type="submit" value="submit">
      </form>
   </body>
</html>

templates/result.html

<!DOCTYPE html>
<html>
   <body>
      <h1>Second Page</h1>
      <p> Your message was: {{ msg }} </p>
   </body>
</html>

Solution 2 - Passing data as part of the query string

Again, please note the risks that may come with this approach (when using query parameters), as mentioned above and discussed in this answer.

app.py

from fastapi import FastAPI, Request, Depends
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel

app = FastAPI()
templates = Jinja2Templates(directory='templates')
 

class Item(BaseModel):
    msg: str


@app.post('/submit')
def get_result(request: Request, item: Item = Depends()):
    context = {'request': request, 'msg': item.msg}
    return templates.TemplateResponse('result.html', context)

   
@app.get('/')
def index(request: Request):
    return templates.TemplateResponse('index.html', {'request': request})

templates/index.html

<!DOCTYPE html>
<html>
   <body>
      <h1>First Page</h1>
      <form method="post" id="myForm" onsubmit="transformFormData();" enctype="multipart/form-data">
         Message : <input type="text" name="msg" value="foo"><br>
         <input type="submit" value="submit">
      </form>
      <script>
         function transformFormData(){
            var myForm = document.getElementById('myForm');
            var qs = new URLSearchParams(new FormData(myForm)).toString();
            myForm.action = 'http://127.0.0.1:8000/submit?' + qs;
         }
      </script>
   </body>
</html>

templates/result.html

<!DOCTYPE html>
<html>
   <body>
      <h1>Second Page</h1>
      <p> Your message was: {{ msg }} </p>
   </body>
</html>
0
Speedy Gonzales On

after brainstorming I found this solution for myself

the task: There are three comboboxes on the page, I select the first and second, the values of the third depend on the values of the first two. The values for the third i are obtained from the database. After receiving the values of the first and second, I am redirected where I create a dictionary and pass it to the initial method. Then I open the page with the combobox again and fill the third combobox with the values from the database

I hope I explained my task clearly

1) initial method:

@router.get(path="/persona_id={persona_id}/add-new", response_class=responses.HTMLResponse,)
async def create_job_history_init(request: Request, persona_id: int,
    schedule_service: DependsSchedule, staff_service: DependsStaff,
    context_ext: Optional[str] = None,
):
    context = {}
    status_code: status
    related = await staff_service.get_all_related_items()
    context["request"] = request
    context["persona_id"] = persona_id
    if context_ext is not None:
        tmp = eval(context_ext)
        context = context | qwe
        schedule = await staff_service.get_item_by_where(
            dep_id=tmp["department_id"], div_id=tmp["division_id"]
        )
        context["staff_schedule"] = schedule
        status_code = status.HTTP_200_OK
    else:
        schedule = await schedule_service.get_all_item()
        context = context | schedule | related
        status_code = status.HTTP_200_OK

    return backend.webapps.templates.TemplateResponse(
        name="job_history/create_job_history.html",
        context=context,
        status_code=status_code,
    )

2) in the browser we get

unfortunately I don't have enough rights to post pictures

there are 3 comboboxes in the picture

3) using JS I create a redirect so it’s seamless

function cmb_pos() {
        var data = {
            'department_id': dep_id, 'division_id': div_id,
        }
        fetch('/job-history/add-new/intermediate/{{persona_id}}', {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(data)
        })
            .then(response => response.json())
            .then(response => console.log(JSON.stringify(response)))
        /* { window.location.reload(); } */
    };

4) method that accepts a redirect

@router.post(path="/add-new/intermediate/{persona_id}", response_class=responses.RedirectResponse,)
async def ceate_job_history_intermediate(request: Request, item: JobHistoryIntermediate, persona_id: int):
    context_ext = {}
    context_ext["flag"] = "True"
    context_ext["department_id"] = item.department_id
    context_ext["division_id"] = item.division_id

    redirect_url = request.url_for("create_job_history_init", persona_id=persona_id)
    parsed = list(urllib.parse.urlparse(redirect_url._url))
    parsed[4] = urllib.parse.urlencode({**{"context_ext": context_ext}})
    redirect_url = urllib.parse.urlunparse(parsed)

    return responses.RedirectResponse(
        url=redirect_url,
        status_code=status.HTTP_303_SEE_OTHER,
    )

variable parsed is:

parsed
['http', '127.0.0.1:8000', '/job-history/persona...=3/add-new', '', '', '']
special variables
function variables
0: 'http'
1: '127.0.0.1:8000'
2: '/job-history/persona_id=3/add-new'
3: ''
4: ''
5: ''
len(): 6

after adding the dictionary:

parsed
['http', '127.0.0.1:8000', '/job-history/persona...=3/add-new', '', 'context_ext=%7B%27fl...27%3A+1%7D', '']
special variables
function variables
0: 'http'
1: '127.0.0.1:8000'
2: '/job-history/persona_id=3/add-new'
3: ''
4: 'context_ext=%7B%27flag%27%3A+%27True%27%2C+%27department_id%27%3A+1%2C+%27division_id%27%3A+1%7D'
5: ''
len(): 6

the address will look like this:

INFO: 127.0.0.1:59947 - "GET /job-history/persona_id%3D3/add-new?context_ext=%7B%27flag%27%3A+%27True%27%2C+%27department_id%27%3A+1%2C+%27division_id%27%3A+1%7D HTTP/1.1" 200 OK
  1. then create_job_history_init is called and the data from the database is transferred to the browser

but I still have one problem I can't reload the page with new data