pointfreeco/vapor-routing

How can I specify HTTP Method in my routes

anhar opened this issue · 6 comments

anhar commented

Hi y'all!

I saw the fantastic video on the PointFree website demonstrating the ability of this library and I wanted to play around with it.
I've got myself a simple little Vapor app with a House entity and Room entity in order to have a one-to-many relation.

I'm setting up my router like this:

func setupRouter(app: Application) throws {
    let houseRouter = OneOf {
        Route(.case(HouseRoute.house)) {
            Path { "houses"; UUID.parser() }
        }
        Route(.case(HouseRoute.create)) {
            Path { "houses"; }
        }
    }
    app.mount(houseRouter, use: houseHandler)
}

And then I have my handler function looks like this:

func houseHandler(request: Request,
                  route: HouseRoute) async throws -> any AsyncResponseEncodable {
    switch route {
    case let .house(houseId: houseId):
        guard let house: House = try await House.query(on: request.db)
            .filter(\House.$id == houseId)
            .first() else {
            throw Abort(.notFound)
        }
        return HouseResponse(house: house)
    case .create:
        let house: House = try request.content.decode(House.self)
        try await house.create(on: request.db)
        return HouseResponse(house: house)
    }
}

When running the request with a POST HTTP method it fails with the following error:
image

However if I run the request with a GET HTTP method it succeeds even though a GET request shouldn't have a HTTP body according to the spec:
image

Now my question is how do I specify in the setupRouter method that the HouseRoute.create enum case maps to a POST HTTP method?

anhar commented

Okay so after doing some digging I found out that the root cause of this failing is because of the following line in Route.swift in the swift-url-routing library.

I forked both repositories and updated the method to this which solved the issue:

@inlinable
  public func parse(_ input: inout URLRequestData) throws -> Parsers.Output {
    let output = try self.parsers.parse(&input)
    if input.method != nil {
        if input.method == "GET" {
            try Method.get.parse(&input)
        }
        else if input.method == "POST" {
            try Method.post.parse(&input)
        }
    }
    try PathEnd().parse(input)
    return output
  }

However I'm not sure how the underlying library works and if you want to just go ahead and hardcode the different HTTP methods in a beautiful nested if else statement 🤷‍♂️

@anhar You can use the Method parser:

Route(.case(HouseRoute.create)) {
  Method.post
  Path { "houses" }
}

I found out that the root cause of this failing is because of the following line in Route.swift

The parser uses the GET method by default if it isn't set on the request.

The swift-url-routing Documentation is a good place to start to see what's available. It might be worth checking out the benchmark suite or even the PointFreeRouter for some example use cases.

anhar commented

The parser uses the GET method by default if it isn't set on the request.

That's not true though, the URLRouting.URLRequestData object has its method property set to POST. See the screenshot below:
bug

This code snippet sets the method property to nil somewhere down the callstack:

Route(.case(HouseRoute.create)) {
    Method.post
    Path { "houses"; }
}

See screenshot here:
bug2

@anhar You're right. I meant the parser tries to parse the GET method by default. That way Method.get can be omitted for GET (and HEAD) requests.
The Method parser, will parse the method from the request data which sets it to nil.
Does using Method.post solve the problem or is it still not working as expected?

Hi @anhar, Pat is correct in this situation, and your PR is not how the library should work. It may seem weird, but this logic in Route's parse method is correct:

  public func parse(_ input: inout URLRequestData) throws -> Parsers.Output {
    let output = try self.parsers.parse(&input)
    if input.method != nil {
      try Method.get.parse(&input)
    }
    try PathEnd().parse(input)
    return output
  }

In this method, input is the incoming URL request to your server. If after running the parser on this request the method is still not-nil, then it means that your parser chose not to parse the HTTP method ever (this is because the Method parser consumes the method if it succeeds). So, if that is the case then we force the method to be a GET or HEAD.

So, in your first screenshot it is completely expected that input.method would be POST because your parser did not try parsing the method. And in your second screenshot it is correct for input.method to be nil because then you did start using Method.post to parse the method.

With that said, I do believe you want to specify Method.post in your router for creating a house:

let houseRouter = OneOf {
  Route(.case(HouseRoute.house)) {
    Path { "houses"; UUID.parser() }
  }
  Route(.case(HouseRoute.create)) {
    Method.post
    Path { "houses"; }
  }
}

You can also specify Method.get in the first route, but it is not necessary because it is assumed when left off.

Also I am going to close this issue because it is not a bug in the library, and I am going to move it to a discussion so that we can continue discussing if you have more questions.