Skip to content

Commit

Permalink
Merge pull request #2937 from softwaremill/decode-method-first
Browse files Browse the repository at this point in the history
Decode method inputs first, before path inputs
  • Loading branch information
adamw committed Jun 6, 2023
2 parents fc60aaf + 3a112d9 commit 5504445
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 17 deletions.
14 changes: 0 additions & 14 deletions doc/endpoint/ios.md
Expand Up @@ -208,20 +208,6 @@ alternative name for cookie. Can only be applied for field represented as `Cooki
* `@setCookies` sends several `Set-Cookie` headers. Can only be applied for field represented as `List[Cookie]`
* `@statusCode` sets status code for response. Can only be applied for field represented as `StatusCode`

## Path matching

By default (as with all other types of inputs), if no path input/path segments are defined, any path will match.

If any path input/path segment is defined, the path must match *exactly* - any remaining path segments will cause the
endpoint not to match the request. For example, `endpoint.in("api")` will match `/api`, `/api/`, but won't match
`/`, `/api/users`.

To match only the root path, use an empty string: `endpoint.in("")` will match `http://server.com/` and
`http://server.com`.

To match a path prefix, first define inputs which match the path prefix, and then capture any remaining part using
`paths`, e.g.: `endpoint.in("api" / "download").in(paths)`.

## Status codes

### Arbitrary status codes
Expand Down
1 change: 1 addition & 0 deletions doc/index.md
Expand Up @@ -235,6 +235,7 @@ We offer commercial support for sttp and related technologies, as well as develo
server/armeria
server/aws
server/options
server/path
server/interceptors
server/logic
server/observability
Expand Down
2 changes: 1 addition & 1 deletion doc/server/errors.md
Expand Up @@ -88,7 +88,7 @@ The behavior described in the latter three points can be customised by providing
failing input and failure description can decide, whether to return a "no match" or a specific response.

Only the first failure encountered for a specific endpoint is passed to the `DecodeFailureHandler`. Inputs are decoded
in the following order: path, method, query, header, body.
in the following order: method, path, query, header, body.

Note that the decode failure handler is used **only** for failures that occur during decoding of path, query, body
and header parameters - while invoking `Codec.decode`. It does not handle any failures or exceptions that occur
Expand Down
42 changes: 42 additions & 0 deletions doc/server/path.md
@@ -0,0 +1,42 @@
# Path matching

When a server receives a request, it must determine which endpoint might potentially handle it. In order to do so,
the endpoints are first pre-filtered, so that only endpoints where the path shape matches (that is, the number of
path inputs/segments must match, as well as any constant segments) are considered.

Next, the inputs are decoded, starting from the method. If the method inputs decode successfully, the path inputs
are decoded.

## Exact matches and trailing slashes

The path must match *exactly* - any remaining path segments will cause the endpoint not to match the request.
However, extra trailing slashes are allowed. For example, `endpoint.in("api")` will match `/api`, `/api/`, but won't
match `/`, `/api/users`.

To match only the root path, use an empty string: `endpoint.in("")` will match `http://server.com/` and
`http://server.com`.

## Matching any path

As with all other types of inputs, if no path input/path segments are defined, any path will match.

To match a path prefix, first define inputs which match the path prefix, and then capture any remaining part using
`paths`, e.g.: `endpoint.in("api" / "download").in(paths)`.

## Decoding failures

If decoding a path input fails, a `400 Bad Request` will be returned to the user. When using the default decode
failure handler, this can be customised to instead attempt decoding the next endpoint, by adding an attribute to the
path input with `.onDecodeFailureNextEndpoint`.

Alternatively, another strategy can be implemented by using a completely custom decode failure handler. Both
topics are covered in more detail in the documentation of [error handling](errors.md).

## Endpoint ordering

The order in which endpoints are given to the server interpreter matters. If the shape of multiple endpoints matches
certain requests, such endpoints should be listed from the most specific, to the least specific.

For example, an endpoint `endpoint.in("users" / "find")` is more specific than `endpoint.in("users" / path[Int]("id"))`,
and should be listed first: otherwise attempting to decode `"find"` as an integer will cause an error. More complex
scenarios of path matching can be implemented using the approach described in the previous section.
Expand Up @@ -91,7 +91,7 @@ object DecodeBasicInputs {
matchWholePath: Boolean = true
): (DecodeBasicInputsResult, DecodeInputsContext) = {
// The first decoding failure is returned.
// We decode in the following order: path, method, query, headers (incl. cookies), request, status, body
// We decode in the following order: method, path, query, headers (incl. cookies), request, status, body
// An exact-path check is done after path & method matching

val basicInputs = input.asVectorOfBasicInputs().zipWithIndex.map { case (el, i) => IndexedBasicInput(el, i) }
Expand All @@ -103,8 +103,8 @@ object DecodeBasicInputs {
// we're using null as a placeholder for the future values. All except the body (which is determined by
// interpreter-specific code), should be filled by the end of this method.
compose(
matchPath(pathInputs, _, _, matchWholePath),
matchOthers(methodInputs, _, _),
matchPath(pathInputs, _, _, matchWholePath),
matchOthers(otherInputs, _, _)
)(DecodeBasicInputsResult.Values(Vector.fill(basicInputs.size)(null), None), ctx)
}
Expand Down
Expand Up @@ -626,6 +626,22 @@ class ServerBasicTests[F[_], OPTIONS, ROUTE](
) { (backend, baseUri) =>
basicStringRequest.post(uri"$baseUri/animal/bird").send(backend).map(_.body shouldBe "This is a bird")
}
},
testServer(
"two endpoints with different methods, first one with path parsing",
NonEmptyList.of(
route(
List[ServerEndpoint[Any, F]](
endpoint.get.in("p1" / path[Int]("id")).serverLogic((_: Int) => pureResult(().asRight[Unit])),
endpoint.post.in("p1" / path[String]("id")).serverLogic((_: String) => pureResult(().asRight[Unit]))
)
)
)
) { (backend, baseUri) =>
basicRequest.get(uri"$baseUri/p1/123").send(backend).map(_.code shouldBe StatusCode.Ok) >>
basicRequest.get(uri"$baseUri/p1/abc").send(backend).map(_.code shouldBe StatusCode.BadRequest) >>
basicRequest.post(uri"$baseUri/p1/123").send(backend).map(_.code shouldBe StatusCode.Ok) >>
basicRequest.post(uri"$baseUri/p1/abc").send(backend).map(_.code shouldBe StatusCode.Ok)
}
)

Expand Down

0 comments on commit 5504445

Please sign in to comment.