Skip to content

ResourceCollection.make_path() incorrectly adds trailing slash to terminal endpoints #31

@ngjunsiang

Description

@ngjunsiang

ResourceCollection.make_path(part) always adds a trailing slash, even for terminal endpoints that shouldn't have one. This causes 405 Method Not Allowed errors when the client makes requests to servers with strict_slashes=True.

Problem

In interface.py, the ResourceCollection.make_path() method:

def make_path(self, part: str | None = None) -> str:
    """Create a full path for a resource collection.

    Resource collection paths always end in a /.
    """
    if part:
        return (
            f"/{self.root.make_path(self.path).strip(SLASH)}"
            f"/{part.strip(SLASH)}/"
        )
    else:
        return f"/{self.root.make_path(self.path).strip(SLASH)}/"

This unconditionally adds a trailing slash when part is provided. However, this is incorrect for terminal endpoints (like /authorization_code) that are actions, not resource collections.

Impact

When CampusSessions.from_code() calls:

resp = self.client.post(
    self.make_path("authorization_code"),
    json={"code": code}
)

It generates /sessions/campus/authorization_code/ (with trailing slash), but the Flask route is defined as POST /sessions/<provider>/authorization_code (without trailing slash).

With strict_slashes=True on the server (which is needed to avoid 308 redirects that strip headers), this results in:

405 Method Not Allowed: The method is not allowed for the requested URL.

Suggested Fix

Add an end_slash parameter to ResourceCollection.make_path(), similar to Resource.make_path():

def make_path(self, part: str | None = None, end_slash: bool = True) -> str:
    """Create a full path for a resource collection.

    Args:
        part: Optional sub-resource or action path.
        end_slash: Whether to add a trailing slash (default: True for collections).

    Returns:
        Full path for the resource collection or sub-resource.
    """
    if part:
        base = f"/{self.root.make_path(self.path).strip(SLASH)}/{part.strip(SLASH)}"
        return f"{base}/" if end_slash else base
    else:
        return f"/{self.root.make_path(self.path).strip(SLASH)}/"

Then update from_code() to use end_slash=False:

def from_code(self, code: str) -> campus.model.AuthSession:
    """Get a session using authorization code."""
    resp = self.client.post(
        self.make_path("authorization_code", end_slash=False),
        json={"code": code}
    )
    # ...

Alternative

If changing the API is too disruptive, consider adding a separate method like make_action_path() for terminal endpoints that don't need trailing slashes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions