Fastapi: [FEATURE] ending / for collections

Created on 22 Feb 2019  路  13Comments  路  Source: tiangolo/fastapi

Is your feature request related to a problem? Please describe.

If we use the following option with sub-routes

app.include_router(search_router, prefix="/items")

We are forced to have an ending '/' on the collection endpoints, e.g. /items/ vs /items/{id} This is I think the more formal REST practice, but it results in a 404 when someone uses /items instead - I'd rather follow the mantra of 'be flexible with regards to user input and strict with what I return'. I'd like to have the option of allowing both /items and /items/ for the collection endpoints.

Describe the solution you'd like

I'd be happier to have a single route documented and the other route just work. I do like the setup of the prefixing of sub-routes - that's really convenient and powerful. But, I need to have the /items collection route option which prefix'ing doesn't allow for.

Describe alternatives you've considered

I can provide an /items endpoint without using the prefix option. I don't know if there is a better way to handle this though. I also managed to add two route decorators to get both /items and /items/ working as endpoints and documented in Swagger - unfortunately Swagger doesn't support synonymous routes yet. I'd be happier to have a single route documented and the other route just work.

Additional context

https://take.ms/fkG7N

enhancement

Most helpful comment

@wozniakty yep, thanks. It's updated now.

@teuneboon if you have declared a path operation with /users it should work as-is. If it's /users/ then Starlette/FastAPI will try to redirect.

If it's the main path operation under a router for /users, you can now declare a path of "" instead of "/" and it will accept the /users even if that's a sub-router.

All 13 comments

I had this issue as well, in the end I stopped using prefixes so I could have the /items endpoint. Not ideal...

We are forced to have an ending '/' on the collection endpoints, e.g. /items/ vs /items/{id} This is I think the more formal REST practice, but it results in a 404 when someone uses /items instead - I'd rather follow the mantra of 'be flexible with regards to user input and strict with what I return'. I'd like to have the option of allowing both /items and /items/ for the collection endpoints.

Sorry in advance if I'm totally off, maybe I don't get what you mean, but I don't seem to have the issue you describe.

If for instance I have that users router, using prefix as described

app = FastAPI()
app.include_router(users_router, prefix='/users')

then for instance, inside my users router I have those routes for instance:

router = APIRouter()


@router.get("/list", response_model=List[User], tags=["Users"])
async def list():
    query = users.select()
    result = await database.fetch_all(query)
    return [r._row for r in result]


@router.get("/me", response_model=User, tags=["Users"])
async def users_me(current_user: User = Depends(get_current_active_user)):
    return current_user

As you'll see in image attached none of the routes end with a slash, or that's what you'd expect ?

Imgur

Typically - one would use the GET /users to list the users and GET /users/{id} to get one user specified by id

Using the prefix option, if you use /users as the prefix and '/' as the path in the sub-route module, this results in /users/ and no way to generate /users.

I guess that prefix wasn't really designed for this purpose and is more for something like /api/v1 as the prefix and then add in the /users in the sub-route user module.

I think I understand the problem.

First: declaring a path operation /users/, and having a client request /users, should (optionally) redirect to or return the same as /users/.

Then, using include_router, should have the same behavior.


I think there is even code in Starlette (although not documented) to handle it.

I'll check it as soon as I'm in front of my laptop (I'm on a trip for a week).

Tested and indeed redirect happens using a curl -L to follow redirects:

 curl -L -v http://127.0.0.1:8001/users   
* Expire in 0 ms for 6 (transfer 0x563800ebcd30)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x563800ebcd30)
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /users HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< server: uvicorn
< date: Mon, 25 Feb 2019 16:05:22 GMT
< location: http://127.0.0.1:8001/users/
< transfer-encoding: chunked
< 
* Ignoring the response-body
* Connection #0 to host 127.0.0.1 left intact
* Issue another request to this URL: 'http://127.0.0.1:8001/users/'
* Found bundle for host 127.0.0.1: 0x563800ebbd60 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host 127.0.0.1
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
* Expire in 0 ms for 6 (transfer 0x563800ebcd30)
> GET /users/ HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< server: uvicorn
< date: Mon, 25 Feb 2019 16:05:22 GMT
< content-length: 16
< content-type: application/json
< 
* Connection #0 to host 127.0.0.1 left intact
{"users":"root"}%         

server-side log:

backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - Connected
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [8] Initialized {'type': 'http', 'http_version': '1.1', 'server': ('172.28.0.2', 8000), 'client': ('172.28.0.1', 59822), 'scheme': 'http', 'method': 'GET', 'root_path': '', 'path': '/users', 'query_string': b'', 'headers': '<...>'}
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [8] Started task
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [8] Received {'type': 'http.response.start', 'status': 302, 'headers': '<...>'}
backend_1_79e424196ea0 | INFO:uvicorn:('172.28.0.1', 59822) - "GET /users HTTP/1.1" 302
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [8] Received {'type': 'http.response.body', 'body': '<0 bytes>'}
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [8] Completed
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [9] Initialized {'type': 'http', 'http_version': '1.1', 'server': ('172.28.0.2', 8000), 'client': ('172.28.0.1', 59822), 'scheme': 'http', 'method': 'GET', 'root_path': '', 'path': '/users/', 'query_string': b'', 'headers': '<...>'}
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [9] Started task
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [9] Received {'type': 'http.response.start', 'status': 200, 'headers': '<...>'}
backend_1_79e424196ea0 | INFO:uvicorn:('172.28.0.1', 59822) - "GET /users/ HTTP/1.1" 200
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [9] Received {'type': 'http.response.body', 'body': '<16 bytes>'}
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - ASGI [9] Completed
backend_1_79e424196ea0 | DEBUG:uvicorn:('172.28.0.1', 59822) - Disconnected

Yep. I think @euri10 is right.

It seems to be working correctly now.


More details:

In recent versions of Starlette, that is handled automatically, as you expect it. And I have updated FastAPI recently, to be compatible with/pinned to the latest versions of Starlette.

Please, update your version of FastAPI with, e.g.:

pip install --upgrade fastapi

And let me know if it's working for you @wshayes and @alexiri.


To test it, you can just copy-paste the files for the tutorial: https://fastapi.tiangolo.com/tutorial/bigger-applications/ (or copy them directly form the source: https://github.com/tiangolo/fastapi/tree/master/docs/src/bigger_applications)

And then open your browser at: http://127.0.0.1:8000/users.

It should redirect you to: http://127.0.0.1:8000/users/

And return a JSON like:

[
  {"username":"Foo"},
  {"username":"Bar"}
]

I forgot to mention, thanks @euri10 for your help here. As always, greatly appreciated! :taco: :tada: :cake:

I'm just tuning in (to the project and this issue), but it seems everyone concluded that Starlette is responsible and seems to have added redirects when you miss a slash (but not when you provide an unnecessary one). But even so, does that solve the case from @wshayes' original screenshot with POST requests? I still lose that data and it's remapped to GET.

If I'm not wrong about that, here are some ideas:

  1. I commented on a Starlette issue suggesting that we not care about trailing slashes at all. Can anyone think of a downside to that?
  2. In the meantime, I figured I'd try adding a /? to all my route decorators, and that seems to work great for all cases! However, then the docs include this trailing slash. Perhaps it'd be sufficient to clean that up before generating docs?
<img width="449" alt="Screen Shot 2019-05-04 at 10 16 23 PM" src="https://user-images.githubusercontent.com/1664041/57187353-0a2a6100-6ebb-11e9-85b8-0aa97d405aab.png">

Ironically, I had trouble loading the docs at first. I was getting a 404 and assumed I had forgotten some plugin... then I realized I was calling /docs/ and it didn't honor the trailing slash. ;)

Loving the project so far! Let me know how I can help.

@hjkelly Yep, it's the thing with REST, that it's not very strict (sometimes it's good, sometimes confusing).

About use cases for endpoints without trailing slash, sometimes trailing slashes are used for plurals (items) and non-trailing for things that are single, or that don't return several items (or several X). It might also be useful for declaring actions, like, let's say /publish.

About the specific case of having /docs/ and /redoc/ instead of /docs and /redoc, I would consider it. Would you like to create a new issue adding it as a feature request? I would like to see if anyone opposes to it, I want to check if that would break any use case.

@tiangolo Although this is closed, just calling out that starlette just fixed an issue with redirects on non-get methods that would be great to get in someday:
https://github.com/encode/starlette/pull/563/commits/3f702238247c99089eac76880965b32bd050f3cb
It requires upgrading starlette to 0.12.7 but that's incompatible with the pinned version in FastAPI.

I'm not sure if this is the right place to ask this, but it looks like the redirect that happens here breaks our use-case. We host our api at https://url/api, using Apache to make sure /api points to FastAPI, FastAPI doesn't receive that it's behind /api(it just gets a request to /users/ instead of /api/users/). For the docs to still work in this case I add openapi_prefix='/api' to the FastAPI definition. Now, that's all fine and working, but if I request http://url/api/users (no trailing slash) FastAPI wants to try and redirect it to http://url/users/, which is invalid and gives a 404. Is it possible to tell FastAPI/Starlette that we're running behind /api for redirects like this?

@teuneboon

You can currently fix this behavior by subclassing fastapi.APIRouter and overriding APIRouter.__call__ with a change to this line:
https://github.com/encode/starlette/blob/master/starlette/routing.py#L610

Then use the subclassed router to decorate your endpoints.

But it shouldn't be so awkward to specify a modified redirect path in this way. I think you should create an issue for this in starlette, I think it would probably be well-received, and based on the fact that that method isn't overridden at all in fastapi, fixing it in starlette would probably be a prerequisite for incorporating the fix into fastapi.

@wozniakty yep, thanks. It's updated now.

@teuneboon if you have declared a path operation with /users it should work as-is. If it's /users/ then Starlette/FastAPI will try to redirect.

If it's the main path operation under a router for /users, you can now declare a path of "" instead of "/" and it will accept the /users even if that's a sub-router.

Was this page helpful?
0 / 5 - 0 ratings