-
Notifications
You must be signed in to change notification settings - Fork 925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle a fragment in a client-side URI properly #4789
Conversation
123224c
to
6e104ea
Compare
|
ba1a2be
to
6c2b215
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file was actually renamed from PathParsingBenchmark
.
6c2b215
to
df6c639
Compare
@@ -166,6 +167,7 @@ final class HttpClientFactory implements ClientFactory { | |||
this.options = options; | |||
|
|||
clientDelegate = new HttpClientDelegate(this, addressResolverGroup); | |||
RequestTargetCache.registerClientMetrics(meterRegistry); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I split the cache into server- and client-side ones because they parse request targets differently (absolute forms and fragments notably).
4b5bc64
to
4ebeb31
Compare
if (req != ctx.request()) { | ||
return earlyFailedResponse( | ||
new IllegalStateException("ctx.request() does not match the actual request; " + | ||
"did you forget to call ctx.updateRequest() in your decorator?"), | ||
ctx, req); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By making sure req == ctx.request()
, we don't need to validate req.path
anymore below because ctx.updateRequest()
or ctx.newDerivedContext()
validated it already.
I need to add more test cases to |
@@ -151,6 +151,7 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex | |||
final ContentPreviewer requestContentPreviewer = | |||
contentPreviewerFactory.requestContentPreviewer(ctx, req.headers()); | |||
req = setUpRequestContentPreviewer(ctx, req, requestContentPreviewer, requestPreviewSanitizer); | |||
ctx.updateRequest(req); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😱
core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java
Show resolved
Hide resolved
9788547
to
42dc604
Compare
core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, looks good.
core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java
Outdated
Show resolved
Hide resolved
6be84c6
to
6b9739b
Compare
1c84dbc
to
40bf116
Compare
72f1512
to
75c5f45
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the cleanup and organization 👍 The new changes look much cleaner. Thanks ! 🙇 👍 🚀
return newDerivedContext(id, req, rpcReq, newHeaders, sessionProtocol(), endpoint, newPath); | ||
if (reqTarget.form() != RequestTargetForm.ABSOLUTE) { | ||
// Not an absolute URI. | ||
return new DefaultClientRequestContext(this, id, req, rpcReq, endpoint, null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question) This is probably unrelated to this PR since the old behavior was also like this, but I'm wondering if we should also update the path here as well
final HttpRequest newReq = req.withHeaders(req.headers().toBuilder().path(reqTarget.pathAndQuery()));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can leave it as it is for now because
- there's currently no way for the caller to tell whether the request they specified has been normalized or not. We need to make more complex change for that, which is probably beyond the scope of this PR.
- there's performance penalty for rebuilding the headers and its enclosing request.
if (req == null) { | ||
throw connectionError(PROTOCOL_ERROR, "received a DATA Frame for an unknown stream: %d", | ||
streamId); | ||
if (encoder == null || encoder.findStream(streamId) == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't think of this check 😅 Looks really nice 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good so far. 👍
Let me review again after adding the guard logic to the updateRequest.
core/src/main/java/com/linecorp/armeria/common/AbstractRequestContextBuilder.java
Outdated
Show resolved
Hide resolved
.. which was effectively impossible to use an absolute-form target anyway because of the check in `HttpClientDelegate`
24f18aa
to
288899e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's now more intuitive. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! 🙇♂️ 👍
core/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java
Outdated
Show resolved
Hide resolved
…er.java Co-authored-by: Ikhun Um <[email protected]>
Thanks a lot! 🙇 |
Motivation: When a user sends a request whose path contains a fragment (e.g. `#foo`), Armeria behaves inconsistently depending on whether a user specified an absolute URI in `:path` or not. On an absolute URI, we rely on `URI` for parsing, which takes a fragment into account. Otherwise, we use `PathAndQuery`, which doesn't treat a fragment as a fragment and just normalizes `#` into `%2A` as a part of path and query. Modifications: - Evolve `PathAndQuery` into `RequestTarget` that is capable of parsing and normalizing a `:path` header. - `RequestTarget` now understands a fragment as well as an absolute URI. - Added `RequestTargetForm` - When normalizing a client-side path, `RequestTarget` doesn't clean up consecutive slashes anymore, e.g. `foo///bar` is *not* normalized into `foo/bar` on the client side. - Replaced `path`, `query` and `fragment` fields and parameters with `RequestTarget` where applicable, including: - `RequestContext` implementations - `AbstractRequestContextBuilder` and its subtypes - `UserClient` and its subtypes - `RoutingContext` implementations - Removed `RoutingStatus.INVALID_PATH` because `RequestTarget` always ensures that the path is valid now. - Split the path cache metrics into client-side and server-side ones - Old meter name: `armeria.server.parsed.path.cache` - New meter names: - `armeria.path.cache{type=client}` - `armeria.path.cache{type=server}` - `HttpClientDelegate` now makes sure `ctx.request() == req` to prevent the loophole where a decorator can send a request with an invalid path. - A user must call `ctx.updateRequest()` to validate the request first. - Renamed `PathParsingBenchmark` to `RequestTargetBenchmark` - Added client-side benchmarks - Fixed the incorrect JVM system property name Result: - Armeria client now handles the fragment part of URI consistently. - (Defect) Closed the loophole that allowed a decorator to send a different request than `ctx.request()`. - A decorator now must call `ctx.updateRequest()` when it replaces the current request. - (Improvement) Armeria client doesn't normalize consecutive slashes in a client request path anymore, giving a user freedom to send such a request. - Note: Please make sure that your server or service handles consecutive slashes (e.g. `foo//bar`) properly before an upgrade. Armeria server always cleans up such a path for you, so you don't need to worry. - (Breaking) `RoutingStatus.INVALID_PATH` has been removed because Armeria doesn't leak a request with an invalid state into router. - (Breaking) The signatures of `UserClient.execute()` have been changed. - (Breaking) The names of path cache meters have been changed. - Old meter name: `armeria.server.parsed.path.cache` - New meter names: - `armeria.path.cache{type=client}` - `armeria.path.cache{type=server}`
Motivation:
When a user sends a request whose path contains a fragment (e.g.
#foo
),Armeria behaves inconsistently depending on whether a user specified an
absolute URI in
:path
or not.On an absolute URI, we rely on
URI
for parsing, which takes a fragmentinto account. Otherwise, we use
PathAndQuery
, which doesn't treata fragment as a fragment and just normalizes
#
into%2A
as a part ofpath and query.
Modifications:
PathAndQuery
intoRequestTarget
that is capable of parsingand normalizing a
:path
header.RequestTarget
now understands a fragment as well as an absolute URI.RequestTargetForm
RequestTarget
doesn't clean upconsecutive slashes anymore, e.g.
foo///bar
is not normalized intofoo/bar
on the client side.path
,query
andfragment
fields and parameters withRequestTarget
where applicable, including:RequestContext
implementationsAbstractRequestContextBuilder
and its subtypesUserClient
and its subtypesRoutingContext
implementationsRoutingStatus.INVALID_PATH
becauseRequestTarget
alwaysensures that the path is valid now.
armeria.server.parsed.path.cache
armeria.path.cache{type=client}
armeria.path.cache{type=server}
HttpClientDelegate
now makes surectx.request() == req
to preventthe loophole where a decorator can send a request with an invalid path.
ctx.updateRequest()
to validate the request first.PathParsingBenchmark
toRequestTargetBenchmark
Result:
request than
ctx.request()
.ctx.updateRequest()
when it replacesthe current request.
client request path anymore, giving a user freedom to send such a request.
slashes (e.g.
foo//bar
) properly before an upgrade. Armeria serveralways cleans up such a path for you, so you don't need to worry.
RoutingStatus.INVALID_PATH
has been removed becauseArmeria doesn't leak a request with an invalid state into router.
UserClient.execute()
have been changed.armeria.server.parsed.path.cache
armeria.path.cache{type=client}
armeria.path.cache{type=server}