pythonslackfastapislack-apislack-commands

Use FastAPI to parse incoming POST request from Slack


I'm building a FastAPI server to receive requests sent by slack slash command. Using the code below, I could see that the following:

token=BLAHBLAH&team_id=BLAHBLAH&team_domain=myteam&channel_id=BLAHBLAH&channel_name=testme&user_id=BLAH&user_name=myname&command=%2Fwhatever&text=test&api_app_id=BLAHBLAH&is_enterprise_install=false&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%BLAHBLAH&trigger_id=BLAHBLAHBLAH

was printed, which is exactly the payload I saw in the official docs. I'm trying to use the payload information to do something, and I'm curious whether there's a great way of parsing this payload info. I can definitely parse this payload using the split() function or any other beautiful functions, but I'm curious whether there is a "de facto" way of dealing with slack payload. Thanks in advance!

from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/")
async def root(request: Request):
    request_body = await request.body()
    print(request_body)

Solution

  • Receive JSON data

    You would normally use Pydantic models to declare a request body—if you were about to receive data in JSON format—thus, benefiting from the automatic validation that Pydantic has to offer (for more options on how to post JSON data, have a look at this answer). In Pydantic V2 the dict() method has been replaced by model_dump(), in case you had to convert the model into a dictionary. So, you would have to define a Pydantic model like this:

    from fastapi import FastAPI
    from pydantic import BaseModel
    
    
    class Item(BaseModel):
        token: str
        team_id: str
        team_domain: str
        # etc.
    
    
    app = FastAPI()
    
    
    @app.post("/")
    def root(item: Item):
        print(item.model_dump())  # convert into dict (if required)
        return item
    

    The payload would look like this:

    {
        "token": "gIkuvaNzQIHg97ATvDxqgjtO"
        "team_id": "Foo",
        "team_domain": "bar",
        # etc.
    }
    

    Receive Form data

    If, however, you were about to receive the payload as Form data, just like what slack API does (as shown in the link you provided), you could use Form fileds. With Form fields, your payload will still be validated against those fields and the type you define them with. You would need, however, to define all the parameters in the endpoint, as described in the above link and as shown below:

    from fastapi import  Form
    
    @app.post("/")
    def root(token: str = Form(...), team_id: str = Form(...), team_domain: str = Form(...)):
        return {"token": token, "team_id": team_id, "team_domain": team_domain}
    

    or to avoid specifying the parameters in an endpoint, in case you had a great number of Form fields, you could create a custom dependency class (using the @dataclass decorator, for simplicity), which would allow you to define multiple Form fields inside a separate class, and only use that class definition in your endpoint—see this answer and this answer for more details on FastAPI dependencies. Example:

    from dataclasses import dataclass
    from fastapi import FastAPI, Form, Depends
    
    @dataclass
    class Item:
        token: str = Form(...)
        team_id: str = Form(...)
        team_domain: str = Form(...)
        #...
    
    
    app = FastAPI()
    
    
    @app.post("/")
    def root(data: Item = Depends()):
        return data
    

    As of FastAPI 0.113.0 (see the relevant documentation as well), support has been added for decalring Form fields with Pydantic models (hence, no need for using a @dataclass as shown above):

    from fastapi import FastAPI, Form, Depends
    from pydantic import BaseModel
    
    
    class Item(BaseModel):
        token: str
        team_id: str
        team_domain: str
        #...
    
    
    app = FastAPI()
    
    
    @app.post("/")
    def root(data: Item = Form()):
        return data
    

    Notes

    As FastAPI is actually Starlette underneath, even if you still had to access the request body in the way you do in the question, you should rather use methods such as request.json() or request.form(), as described in Starlette documentation, which would allow you to get the request body parsed as JSON or form-data, respectively. Please have a look at this answer and this answer for more details and examples.