How can I specify HTTP Method in my routes
anhar opened this issue · 6 comments
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:
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:
Now my question is how do I specify in the setupRouter
method that the HouseRoute.create enum case maps to a POST
HTTP method?
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.
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:
This code snippet sets the method
property to nil
somewhere down the callstack:
Route(.case(HouseRoute.create)) {
Method.post
Path { "houses"; }
}
@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.