When working with Tiktok API, I leave out an endpoint without the trailing slash (`domain.com/endpoint` instead of `domain.com/endpoint/`). That caused an error of 405 Method Not Allowed and cost me about 4 hours of frustration to recognize this. In this post, I will discuss the following:

  • Why a missing trailing slash can cause the error?
  • When designing API, should we let it with trailing slash or not?

Why a missing trailing slash can cause the error?

Let me provide some context first, I was doing a post request to tiktok, say creating a campaign (for the sake of example) as follow:

url = os.path.join(root_api, "campaign/create")
payload = {"campaign_name": "Test CP"}
resp = session.post(url, json=payload)
Code snippet to create a campaign (the wrong one)

Look nothing wrong, right? But when I ran it, the result is 405 - Method Not Allowed. Hmm, their documentation clearly state this is a post request, and I am doing the post request. So what is wrong here?

After a really long time staring at the snippet, and a lot of tried and error, I discovered that when append a slash at the end of the url, it works normal.

url = os.path.join(root_api, "campaign/create/")
The correct url

It works now, right? Okay, let patch the bug first, recheck all other place and make sure all url had trailing slash. Then let's dig into what causes this problem. Let try to connect things together:

  • I remembered that TikTok encourage us to query to url with trailing slash, even with get request (`domain.com/get/?a=b` instead of `domain.com/get?a=b).
  • When we hit error 405: Method Not Allowed, it means the request is now under new HTTP verb that different from POST
  • The only different between the right code and wrong code is the trailing slash (`/`)

Based on all the above, I suspect that TikTok enforce a convention of all URLs will be ending with trailing slash, and URL without trailing slash will be automatically redirect with appended trailing slash. We don't know their code and configuration, so the only way to validate this speculation is to try to send the request and capture the response.

Based on the error, I think requests library handle the redirect and continue the redirect, so we could not capture the immediate response, so at this state, I will use tool call httpie to replicate the request.

$ http POST https://business-api.tiktok.com/open_api/v1.3/campaign/create Access-Token:d68..redacted... campaign_name=clitest

And this is the response

httpie response capture

Let's focus on the underline line in the image, this endpoint (without trailing slash) returned a redirect to the new URL with added trailing slash. Since the browser (or the requests library) usually follow this redirect, it will automatically go to the destination url, but this time, using GET method instead of the original POST request. Thus, it resulting the error of 405 Method Not Allowed.

Okay, now to replicate this with requests library, added allow_redirects=False and we can capture the 308 response.

url = os.path.join(root_api, "campaign/create")
payload = {"campaign_name": "Test CP"}
resp = session.post(url, json=payload)
resp.status_code # 308
resp.headers.get("Location") # http://business-api.tiktok.com/open_api/v1.3/campaign/create/

Okay, now we know that the server will automatically redirect the url to match the configured url, so a missing slash in URL can cause a redirect or not found error.

When designing API endpoint, should we let it with trailing slash or not?

So, if you are writing API endpoint, what are you configure?

  • Are you supporting url with no trailing slash? What happen if user added a trailing slash
  • Or are you configure url with trailing slash? What happen if user forgot the trailing slash
  • Or are you support both?

Okay, let dive in with me and find out which one should be best.

First, let find out is there any different between these url

https://example.com/foo
https://example.com/foo/

They are not the same URL, caches will store them separately. There is no specific recommendation to use either.

So it comes down to the convention to use in any specific project or team.

In general, a URL with a trailing slash indicates that it refers to a directory or collection of resources, while a URL without a trailing slash typically refers to a specific resource within that directory or collection.

For example, the following trailing slash should be interpreted as a collection of all users in the system

https://example.com/api/users/

Whereas the following without trailing slash might refer to a specific user with ID 123

https://example.com/api/users/123

Also, different framework handle them differently, it is worth checking their documentation and behavior on how they interpret them.

Let try with fastAPI:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items")
async def items():
    return {"message": "items without trailing slash"}


@app.get("/items/")
async def items_2():
    return {"message": "items with trailing slash"}

If both `/items` and `/items/` are registered, hitting each with match to the equivalent snippet:

$ http -b http://localhost:8000/items
{
    "message": "items without trailing slash"
}
$ http -b http://localhost:8000/items/
{
    "message": "items with trailing slash"
}

So, how about we only registered one endpoint in the api server but we hit both variation? Let modify the code like this and try again

from fastapi import FastAPI

app = FastAPI()

@app.get("/items")
async def items():
    return {"message": "items without trailing slash"}

When hitting http://localhost:8000/items it works as expected. So we only need to try with added trailing slash, can you guess what is the response?

$ http http://localhost:8000/items/
HTTP/1.1 307 Temporary Redirect
date: Mon, 27 Feb 2023 04:16:55 GMT
location: http://localhost:8000/items
server: uvicorn
transfer-encoding: chunked
# let try again but with added request parameters
$ http http://localhost:8000/items/\?a=b
HTTP/1.1 307 Temporary Redirect
date: Mon, 27 Feb 2023 04:20:46 GMT
location: http://localhost:8000/items?a=b
server: uvicorn
transfer-encoding: chunked

So, the framework automatically redirect it to the registered endpoint without the slash, but since this is GET request it my be ok. But how about hitting the POST endpoint, should it keep redirect with POST or it redirect with GET instead?

Let try with this code snippet:

from pydantic import BaseModel
from fastapi import FastAPI

app = FastAPI()


class Item(BaseModel):
    name: str

@app.post("/items/")
async def create_item(item: Item):
    return item
# hitting the right endpoint
$ http POST http://localhost:8000/items/ name=example
{
    "name": "example"
}
# itentional ommiting the trailing slash
$ http POST http://localhost:8000/items name=example
HTTP/1.1 307 Temporary Redirect
date: Mon, 27 Feb 2023 04:28:02 GMT
location: http://localhost:8000/items/
server: uvicorn
transfer-encoding: chunked

It works as expect when the URL match, but when we intent to omit the trailing slash, the framework redirect it to the registered one, which is http://localhost:8000/items/. There is an important thing to verify is: does it preserve the method and payload, or does it redirect and change the method? Now, ker tell httpie to follow the redirect

$ http --follow POST  http://localhost:8000/items name=example
HTTP/1.1 200 OK
content-length: 18
content-type: application/json
date: Mon, 27 Feb 2023 04:34:31 GMT
server: uvicorn

{
    "name": "example"
}

In the server console, these are the log when above api call hit

INFO:     127.0.0.1:52737 - "POST /items HTTP/1.1" 307 Temporary Redirect
INFO:     127.0.0.1:52737 - "POST /items/ HTTP/1.1" 200 OK

So, it follow the redirect with the same method, and preserve the payload.

So in this case with fastAPI, the framework automatically handle when the url with or without trailing slash. But it add an extra redirect so the time to receive a response will be longer.

Comparing with the case with Tiktok above, since we don't know how they handle the logic when url missing a slash, but based on the behavior we observed, they did a redirect and it is not preserve the original one, thus resulting a surprised error.

Conclusion

Each endpoint with or without trailing slash will point to different resource, and each framework, web server will handle the redirect by default. It is worth double check your url to match with what your server is required, otherwise, you might run into error since the redirect.

When design API, make sure to be consistent with the URL and clearly state in the documentation to avoid mistake when the frontend hitting the server. The convention for the slash can be:

  • trailing slash when the endpoint is query or point to a collection or directory
  • without trailing slash when it refer to a specific entity in resource.