4

I have a FastAPI application running on my local machine under the URL: http://localhost:8000, using the following Python code:

from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
    "*"
    '''
    "http://localhost:8000/add_points/",
    "http://localhost:8000/check_points/",
    "http://localhost:8000/check_item_points/",
    "http://localhost:8000/redeem_points/"
    '''
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

users = {"matt": 0}
items = {"ticket": 7}


class User(BaseModel):
    name: str
    points: float
    item: str


class AddTransaction(BaseModel):
    name: str
    points: float


class UserPoints(BaseModel):  # anything that extnds this base model is a pyantic model
    name: str
    points: float


class Item(BaseModel):
    name: str
    points: float


# -----Post Requests-----
@app.post("/add_points/")
def add_points(add_transaction: AddTransaction):
    global users
    user_id = add_transaction.name
    points = add_transaction.points
    users[user_id] = users.get(user_id, 0) + points
    return users[user_id]


@app.post("/check_points/")
def check_points(user_points: UserPoints):
    global users
    user_id = user_points.name
    points = user_points.points
    return users[user_id], points


@app.post("/check_item_points/")
def check_item_points(item: Item):
    global items
    item_id = item.name
    points = item.points
    return item[item_id], points


@app.post("/redeem_points/")  # user spends points (they lose points) gain an item
def redeem_points(add_transaction: AddTransaction, user_points: UserPoints, item: Item, user: User):
    global users
    global items
    user_id = add_transaction.name
    user_points = user_points.points
    item_points = item.points
    item_pre = item.name
    item_post = user.item
    if user_points >= item_points:
        user_points == user_points - item_points
        item_post == item_pre
        return users[user_id], users[user_points], users[item_post]
    else:
        return "insufficient funds"


# -----Get Requests-----
@app.get("/")
def read_root():
    return {"Hello": "World"}


# -----Put Requests-----
""""
@app.put("/items/{item_id}")
def update_item(item_id: int, item:Item):
    return {"item_name": item.name, "item_id": item_id}
"""

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

I also have an HTML file with JS script inside that simply sends a POST request to http://localhost:8000/add_points/ upon the click of a button. Here is the code for that:

<!DOCTYPE html>
<html>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

<body>
<br><br><br><span style="text-align: center;"><button id="send">Send Request</button></span>
</body>

<script>
$("#send").on("click", evt => {
    $.post("http://localhost:8000/add_points/",
  {
    name: "string",
    points: 5.0
  },
  function(data, status){
    alert("Data: " + data + "\nStatus: " + status);
  });
});
</script>

</html>

However, when I try to send the POST request I get the following errors in PyCharm:

INFO: 127.0.0.1:49413 - "OPTIONS /add_points/ HTTP/1.1" 400 Bad Request

INFO: 127.0.0.1:49413 - "POST /add_points/ HTTP/1.1" 422 Unprocessable Entity

I understand that at least one of these errors originates from the CORS policy restrictions, however, this project is aimed towards mobile phone users who should not have to install any browser extensions for overriding the policy. Any advice as to how to fix these errors would be greatly appreciated!

EDIT UPDATE:

const url = new URL('localhost:8000/add_points/');
$("#send").on("click", evt => { fetch(url, 
      { 
        method: 'POST', 
       headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}, 
          body: JSON.stringify({"name":"John", "points":50.0}) 
      }).then().catch((error) => { console.log(error.message); }); }); 

I'm still getting a 400 Bad Request error.

Chris
  • 18,724
  • 6
  • 46
  • 80
Tommy Harris
  • 59
  • 1
  • 2
  • Does this answer your question? [How to post JSON data from JavaScript frontend to FastAPI backend?](https://stackoverflow.com/questions/73759718/how-to-post-json-data-from-javascript-frontend-to-fastapi-backend) – Chris Oct 16 '22 at 16:24
  • Please make sure to include `'Content-Type': 'application/json'` in the request headers. Related answers can also be found [here](https://stackoverflow.com/a/71741617/17865804), [here](https://stackoverflow.com/a/73096718/17865804) and [here](https://stackoverflow.com/a/70636163/17865804). As for CORS, see [this](https://stackoverflow.com/a/71805329/17865804) and [this](https://stackoverflow.com/a/73963905/17865804). – Chris Oct 16 '22 at 16:27
  • 1
    .. and you should never have to _override the CORS policy_. This doesn't seem to be a CORS-issue, however. – MatsLindh Oct 16 '22 at 16:33
  • I've made a few edits in my HTML, my on click is now this: const url = new URL('http://localhost:8000/add_points/'); $("#send").on("click", evt => { fetch(url, { method: 'POST', headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}, body: JSON.stringify({"name":"John", "points":50.0}) }).then().catch((error) => { console.log(error.message); }); }); I'm still getting a 400 error (bad request). I'm certain the Python code is correct, so any advice is greatly appreciated. – Tommy Harris Oct 17 '22 at 02:18
  • @Chris Why wouldn't you ever need to override the CORS policy? Especially when traffic may be coming from any IP address. I think his issue is that he's running the server on host="0.0.0.0", port=8000 (i.e. 127.0.0.1:8000), but is executing the HTML code on an arbitrary port (e.g. 127.0.0.1:523723) thus CORs is coming into play. In this case, he'd need to modify CORS origin to either ["*"] or use allow_origin_regex for 127.0.0.1. Should be able to confirm the IP/PORT of incoming requests on the OPTIONS request in the python console. – mattsap Oct 17 '22 at 18:22
  • 1
    @mattsap Please have a look at the [answer below](https://stackoverflow.com/a/74106637/17865804). – Chris Oct 18 '22 at 06:40

1 Answers1

1

If you are accessing the frontend from a Jinja2 template or an HTMLResponse—which seems to be the case, as shown from the port that your app is listening on, as well as the origins you defined in the CORSMiddleware—you don't need to have CORS enabled for your application. As described in this answer and this answer, two objects have the same origin only when the protocol, domain and port all match.

Please note that localhost and 127.0.0.1 are considered to be different origins. Hence, if you are accessing your fronend from the browser (by typing the URL in the address bar) at http://127.0.0.1:8000/, then your asynchronous JS request (using fetch, for instance) should use that domain; for example:

fetch('http://127.0.0.1:8000/add',...

not

fetch('http://localhost:8000/add',...

and vice versa. You can always use relative paths for your fetch requests (as shown below), and hence, your app will work regardless of the domain name you used (either localhost or 127.0.0.1) for accessing the frontend from your browser.

Working Example:

See related answers here, here and here as well.

app.py

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

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

class Item(BaseModel):
    name: str
    points: float

@app.post("/add")
def add_points(item: Item):
    return item
    
@app.get("/")
def index(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

templates/index.html

<!DOCTYPE html>
<html>
   <body>
      <input type="button" value="Submit" onclick="submit()">
      <div id="responseArea"></div>
      <script>
         function submit() {
            fetch('/add', {
                 method: 'POST',
                 headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                 },
                 body: JSON.stringify({'name': 'John', 'points': 50.0})
              })
              .then(resp => resp.text()) // or, resp.json(), etc.
              .then(data => {
                 document.getElementById("responseArea").innerHTML = data;
              })
              .catch(error => {
                 console.error(error);
              });
         }
      </script>
   </body>
</html>
Chris
  • 18,724
  • 6
  • 46
  • 80