Skip to content

Commit 95ed9e1

Browse files
authored
Introduce rawPath in RequestTarget and ServiceRequestContext (#5932)
Motivation: It may be useful if we have access to the original request path unmodified. For example we have use cases where the decoding rules are different from the ones at `decodedPath`. Modifications: - Added `rawPath` method in `RequestTarget` to store the rawPath. - The rawPath is from the unmodified ':path' header at the server side; the client side target, it's always null. - Added `rawPath` method in `ServiceRequestContext`, it's always non null for server-side targets. Result: - Closes #5931. - Users will get access to the raw request path for a request.
1 parent 206872c commit 95ed9e1

File tree

9 files changed

+237
-62
lines changed

9 files changed

+237
-62
lines changed

core/src/main/java/com/linecorp/armeria/common/AbstractRequestContextBuilder.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ protected AbstractRequestContextBuilder(boolean server, RpcRequest rpcReq, URI u
155155
} else {
156156
reqTarget = DefaultRequestTarget.createWithoutValidation(
157157
RequestTargetForm.ORIGIN, null, null, null, -1,
158-
uri.getRawPath(), uri.getRawPath(), uri.getRawQuery(), uri.getRawFragment());
158+
uri.getRawPath(), uri.getRawPath(), null,
159+
uri.getRawQuery(), uri.getRawFragment());
159160
}
160161
}
161162

core/src/main/java/com/linecorp/armeria/common/RequestTarget.java

+8
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ static RequestTarget forClient(String reqTarget, @Nullable String prefix) {
131131
*/
132132
String maybePathWithMatrixVariables();
133133

134+
/**
135+
* Returns the server-side raw path of this {@link RequestTarget} from the ":path" header.
136+
* Unlike {@link #path()}, the returned string is the original path without any normalization.
137+
* For client-side target it always returns {@code null}.
138+
*/
139+
@Nullable
140+
String rawPath();
141+
134142
/**
135143
* Returns the query of this {@link RequestTarget}.
136144
*/

core/src/main/java/com/linecorp/armeria/internal/common/DefaultRequestTarget.java

+36-16
Original file line numberDiff line numberDiff line change
@@ -163,16 +163,8 @@ boolean mustPreserveEncoding(int cp) {
163163
private static final Bytes EMPTY_BYTES = new Bytes(0);
164164
private static final Bytes SLASH_BYTES = new Bytes(new byte[] { '/' });
165165

166-
private static final RequestTarget INSTANCE_ASTERISK = createWithoutValidation(
167-
RequestTargetForm.ASTERISK,
168-
null,
169-
null,
170-
null,
171-
-1,
172-
"*",
173-
"*",
174-
null,
175-
null);
166+
private static final RequestTarget CLIENT_INSTANCE_ASTERISK = createAsterisk(false);
167+
private static final RequestTarget SERVER_INSTANCE_ASTERISK = createAsterisk(true);
176168

177169
/**
178170
* The main implementation of {@link RequestTarget#forServer(String)}.
@@ -233,9 +225,9 @@ public static RequestTarget forClient(String reqTarget, @Nullable String prefix)
233225
public static RequestTarget createWithoutValidation(
234226
RequestTargetForm form, @Nullable String scheme, @Nullable String authority,
235227
@Nullable String host, int port, String path, String pathWithMatrixVariables,
236-
@Nullable String query, @Nullable String fragment) {
228+
@Nullable String rawPath, @Nullable String query, @Nullable String fragment) {
237229
return new DefaultRequestTarget(
238-
form, scheme, authority, host, port, path, pathWithMatrixVariables, query, fragment);
230+
form, scheme, authority, host, port, path, pathWithMatrixVariables, rawPath, query, fragment);
239231
}
240232

241233
private final RequestTargetForm form;
@@ -249,14 +241,16 @@ public static RequestTarget createWithoutValidation(
249241
private final String path;
250242
private final String maybePathWithMatrixVariables;
251243
@Nullable
244+
private final String rawPath;
245+
@Nullable
252246
private final String query;
253247
@Nullable
254248
private final String fragment;
255249
private boolean cached;
256250

257251
private DefaultRequestTarget(RequestTargetForm form, @Nullable String scheme,
258252
@Nullable String authority, @Nullable String host, int port,
259-
String path, String maybePathWithMatrixVariables,
253+
String path, String maybePathWithMatrixVariables, @Nullable String rawPath,
260254
@Nullable String query, @Nullable String fragment) {
261255

262256
assert (scheme != null && authority != null && host != null) ||
@@ -270,6 +264,7 @@ private DefaultRequestTarget(RequestTargetForm form, @Nullable String scheme,
270264
this.port = port;
271265
this.path = path;
272266
this.maybePathWithMatrixVariables = maybePathWithMatrixVariables;
267+
this.rawPath = rawPath;
273268
this.query = query;
274269
this.fragment = fragment;
275270
}
@@ -312,6 +307,12 @@ public String maybePathWithMatrixVariables() {
312307
return maybePathWithMatrixVariables;
313308
}
314309

310+
@Override
311+
@Nullable
312+
public String rawPath() {
313+
return rawPath;
314+
}
315+
315316
@Nullable
316317
@Override
317318
public String query() {
@@ -380,6 +381,21 @@ public String toString() {
380381
}
381382
}
382383

384+
private static RequestTarget createAsterisk(boolean server) {
385+
final String rawPath = server ? "*" : null;
386+
return createWithoutValidation(
387+
RequestTargetForm.ASTERISK,
388+
null,
389+
null,
390+
null,
391+
-1,
392+
"*",
393+
"*",
394+
rawPath,
395+
null,
396+
null);
397+
}
398+
383399
@Nullable
384400
private static RequestTarget slowForServer(String reqTarget, boolean allowSemicolonInPathComponent,
385401
boolean allowDoubleDotsInQueryString) {
@@ -411,7 +427,7 @@ private static RequestTarget slowForServer(String reqTarget, boolean allowSemico
411427
// Reject a relative path and accept an asterisk (e.g. OPTIONS * HTTP/1.1).
412428
if (isRelativePath(path)) {
413429
if (query == null && path.length == 1 && path.data[0] == '*') {
414-
return INSTANCE_ASTERISK;
430+
return SERVER_INSTANCE_ASTERISK;
415431
} else {
416432
// Do not accept a relative path.
417433
return null;
@@ -443,6 +459,7 @@ private static RequestTarget slowForServer(String reqTarget, boolean allowSemico
443459
-1,
444460
matrixVariablesRemovedPath,
445461
encodedPath,
462+
reqTarget,
446463
encodeQueryToPercents(query),
447464
null);
448465
}
@@ -622,7 +639,7 @@ private static RequestTarget slowForClient(String reqTarget,
622639

623640
// Accept an asterisk (e.g. OPTIONS * HTTP/1.1).
624641
if (query == null && path.length == 1 && path.data[0] == '*') {
625-
return INSTANCE_ASTERISK;
642+
return CLIENT_INSTANCE_ASTERISK;
626643
}
627644

628645
final String encodedPath;
@@ -645,7 +662,9 @@ private static RequestTarget slowForClient(String reqTarget,
645662
null,
646663
-1,
647664
encodedPath,
648-
encodedPath, encodedQuery,
665+
encodedPath,
666+
null,
667+
encodedQuery,
649668
encodedFragment);
650669
}
651670
}
@@ -692,6 +711,7 @@ private static DefaultRequestTarget newAbsoluteTarget(
692711
port,
693712
encodedPath,
694713
encodedPath,
714+
null,
695715
encodedQuery,
696716
encodedFragment);
697717
}

core/src/main/java/com/linecorp/armeria/internal/server/DefaultServiceRequestContext.java

+8
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,14 @@ public String decodedMappedPath() {
305305
return routingResult.decodedPath();
306306
}
307307

308+
@Override
309+
public String rawPath() {
310+
final String rawPath = requestTarget().rawPath();
311+
// rawPath should not be null for server-side targets.
312+
assert rawPath != null;
313+
return rawPath;
314+
}
315+
308316
@Override
309317
public URI uri() {
310318
final HttpRequest request = request();

core/src/main/java/com/linecorp/armeria/server/RoutingContext.java

+1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ default RoutingContext withPath(String path) {
169169
oldReqTarget.port(),
170170
pathWithoutMatrixVariables,
171171
path,
172+
path,
172173
oldReqTarget.query(),
173174
oldReqTarget.fragment());
174175

core/src/main/java/com/linecorp/armeria/server/ServiceRequestContext.java

+5
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ default String queryParam(String name) {
335335
*/
336336
String decodedMappedPath();
337337

338+
/**
339+
* Returns the original path without normalization from the ":path" header.
340+
*/
341+
String rawPath();
342+
338343
/**
339344
* Returns the {@link URI} associated with the current {@link Request}.
340345
* Note that this method is a shortcut of calling {@link HttpRequest#uri()} on {@link #request()}.

core/src/main/java/com/linecorp/armeria/server/ServiceRequestContextWrapper.java

+5
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ public String decodedMappedPath() {
121121
return unwrap().decodedMappedPath();
122122
}
123123

124+
@Override
125+
public String rawPath() {
126+
return unwrap().rawPath();
127+
}
128+
124129
@Nullable
125130
@Override
126131
public MediaType negotiatedResponseMediaType() {

core/src/test/java/com/linecorp/armeria/internal/common/DefaultRequestTargetTest.java

+65-45
Original file line numberDiff line numberDiff line change
@@ -143,27 +143,27 @@ void serverShouldAcceptGoodDoubleDotPatterns(String pattern) {
143143
void dotsAndEqualsInNameValueQuery() {
144144
QUERY_SEPARATORS.forEach(qs -> {
145145
assertThat(forServer("/?a=..=" + qs + "b=..=")).satisfies(res -> {
146-
assertThat(res).isNotNull();
147-
assertThat(res.query()).isEqualTo("a=..=" + qs + "b=..=");
148-
assertThat(QueryParams.fromQueryString(res.query(), true)).containsExactly(
146+
assertThat(res.requestTarget).isNotNull();
147+
assertThat(res.requestTarget.query()).isEqualTo("a=..=" + qs + "b=..=");
148+
assertThat(QueryParams.fromQueryString(res.requestTarget.query(), true)).containsExactly(
149149
Maps.immutableEntry("a", "..="),
150150
Maps.immutableEntry("b", "..=")
151151
);
152152
});
153153

154154
assertThat(forServer("/?a==.." + qs + "b==..")).satisfies(res -> {
155155
assertThat(res).isNotNull();
156-
assertThat(res.query()).isEqualTo("a==.." + qs + "b==..");
157-
assertThat(QueryParams.fromQueryString(res.query(), true)).containsExactly(
156+
assertThat(res.requestTarget.query()).isEqualTo("a==.." + qs + "b==..");
157+
assertThat(QueryParams.fromQueryString(res.requestTarget.query(), true)).containsExactly(
158158
Maps.immutableEntry("a", "=.."),
159159
Maps.immutableEntry("b", "=..")
160160
);
161161
});
162162

163163
assertThat(forServer("/?a==..=" + qs + "b==..=")).satisfies(res -> {
164164
assertThat(res).isNotNull();
165-
assertThat(res.query()).isEqualTo("a==..=" + qs + "b==..=");
166-
assertThat(QueryParams.fromQueryString(res.query(), true)).containsExactly(
165+
assertThat(res.requestTarget.query()).isEqualTo("a==..=" + qs + "b==..=");
166+
assertThat(QueryParams.fromQueryString(res.requestTarget.query(), true)).containsExactly(
167167
Maps.immutableEntry("a", "=..="),
168168
Maps.immutableEntry("b", "=..=")
169169
);
@@ -500,9 +500,9 @@ void clientShouldAcceptAbsoluteUri(String uri,
500500
String expectedScheme, String expectedAuthority, String expectedPath,
501501
@Nullable String expectedQuery, @Nullable String expectedFragment) {
502502

503-
final RequestTarget res = forClient(uri);
504-
assertThat(res.scheme()).isEqualTo(expectedScheme);
505-
assertThat(res.authority()).isEqualTo(expectedAuthority);
503+
final RequestTargetWithRawPath res = forClient(uri);
504+
assertThat(res.requestTarget.scheme()).isEqualTo(expectedScheme);
505+
assertThat(res.requestTarget.authority()).isEqualTo(expectedAuthority);
506506
assertAccepted(res, expectedPath, emptyToNull(expectedQuery), emptyToNull(expectedFragment));
507507
}
508508

@@ -531,15 +531,15 @@ void shouldYieldEmptyStringForEmptyQueryAndFragment(Mode mode) {
531531
@ParameterizedTest
532532
@EnumSource(Mode.class)
533533
void testToString(Mode mode) {
534-
assertThat(parse(mode, "/")).asString().isEqualTo("/");
535-
assertThat(parse(mode, "/?")).asString().isEqualTo("/?");
536-
assertThat(parse(mode, "/?a=b")).asString().isEqualTo("/?a=b");
534+
assertThat(parse(mode, "/").requestTarget).asString().isEqualTo("/");
535+
assertThat(parse(mode, "/?").requestTarget).asString().isEqualTo("/?");
536+
assertThat(parse(mode, "/?a=b").requestTarget).asString().isEqualTo("/?a=b");
537537

538538
if (mode == Mode.CLIENT) {
539-
assertThat(forClient("/#")).asString().isEqualTo("/#");
540-
assertThat(forClient("/?#")).asString().isEqualTo("/?#");
541-
assertThat(forClient("/?a=b#c=d")).asString().isEqualTo("/?a=b#c=d");
542-
assertThat(forClient("http://foo/bar?a=b#c=d")).asString().isEqualTo("http://foo/bar?a=b#c=d");
539+
assertThat(forClient("/#").requestTarget).asString().isEqualTo("/#");
540+
assertThat(forClient("/?#").requestTarget).asString().isEqualTo("/?#");
541+
assertThat(forClient("/?a=b#c=d").requestTarget).asString().isEqualTo("/?a=b#c=d");
542+
assertThat(forClient("http://foo/bar?a=b#c=d").requestTarget).asString().isEqualTo("http://foo/bar?a=b#c=d");
543543
}
544544
}
545545

@@ -572,32 +572,32 @@ void testRemoveMatrixVariables() {
572572
assertThat(removeMatrixVariables("/prefix/;a=b")).isNull();
573573
}
574574

575-
private static void assertAccepted(@Nullable RequestTarget res, String expectedPath) {
575+
private static void assertAccepted(RequestTargetWithRawPath res, String expectedPath) {
576576
assertAccepted(res, expectedPath, null, null);
577577
}
578578

579-
private static void assertAccepted(@Nullable RequestTarget res,
579+
private static void assertAccepted(RequestTargetWithRawPath res,
580580
String expectedPath,
581581
@Nullable String expectedQuery) {
582582
assertAccepted(res, expectedPath, expectedQuery, null);
583583
}
584584

585-
private static void assertAccepted(@Nullable RequestTarget res,
585+
private static void assertAccepted(RequestTargetWithRawPath res,
586586
String expectedPath,
587587
@Nullable String expectedQuery,
588588
@Nullable String expectedFragment) {
589-
assertThat(res).isNotNull();
590-
assertThat(res.path()).isEqualTo(expectedPath);
591-
assertThat(res.query()).isEqualTo(expectedQuery);
592-
assertThat(res.fragment()).isEqualTo(expectedFragment);
589+
assertThat(res.requestTarget).isNotNull();
590+
assertThat(res.requestTarget.path()).isEqualTo(expectedPath);
591+
assertThat(res.requestTarget.query()).isEqualTo(expectedQuery);
592+
assertThat(res.requestTarget.fragment()).isEqualTo(expectedFragment);
593+
assertThat(res.requestTarget.rawPath()).isEqualTo(res.rawPath);
593594
}
594595

595-
private static void assertRejected(@Nullable RequestTarget res) {
596-
assertThat(res).isNull();
596+
private static void assertRejected(RequestTargetWithRawPath res) {
597+
assertThat(res.requestTarget).isNull();
597598
}
598599

599-
@Nullable
600-
private static RequestTarget parse(Mode mode, String rawPath) {
600+
private static RequestTargetWithRawPath parse(Mode mode, String rawPath) {
601601
switch (mode) {
602602
case SERVER:
603603
return forServer(rawPath);
@@ -608,37 +608,57 @@ private static RequestTarget parse(Mode mode, String rawPath) {
608608
}
609609
}
610610

611-
@Nullable
612-
private static RequestTarget forServer(String rawPath) {
611+
private static class RequestTargetWithRawPath {
612+
@Nullable
613+
final String rawPath;
614+
@Nullable
615+
final RequestTarget requestTarget;
616+
617+
RequestTargetWithRawPath(@Nullable String rawPath, @Nullable RequestTarget requestTarget) {
618+
this.rawPath = rawPath;
619+
this.requestTarget = requestTarget;
620+
}
621+
622+
@Override
623+
public String toString() {
624+
return "RequestTargetWithRawPath{" +
625+
"rawPath='" + rawPath + '\'' +
626+
", requestTarget=" + requestTarget +
627+
'}';
628+
}
629+
}
630+
631+
private static RequestTargetWithRawPath forServer(String rawPath) {
613632
return forServer(rawPath, false);
614633
}
615634

616-
@Nullable
617-
private static RequestTarget forServer(String rawPath, boolean allowSemicolonInPathComponent) {
618-
final RequestTarget res = DefaultRequestTarget.forServer(rawPath, allowSemicolonInPathComponent, false);
619-
if (res != null) {
620-
logger.info("forServer({}) => path: {}, query: {}", rawPath, res.path(), res.query());
635+
private static RequestTargetWithRawPath forServer(String rawPath, boolean allowSemicolonInPathComponent) {
636+
final RequestTarget target = DefaultRequestTarget.forServer(
637+
rawPath,
638+
allowSemicolonInPathComponent,
639+
false);
640+
if (target != null) {
641+
logger.info("forServer({}) => path: {}, query: {}", rawPath, target.path(), target.query());
621642
} else {
622643
logger.info("forServer({}) => null", rawPath);
623644
}
624-
return res;
645+
return new RequestTargetWithRawPath(rawPath, target);
625646
}
626647

627-
@Nullable
628-
private static RequestTarget forClient(String rawPath) {
648+
private static RequestTargetWithRawPath forClient(String rawPath) {
629649
return forClient(rawPath, null);
630650
}
631651

632-
@Nullable
633-
private static RequestTarget forClient(String rawPath, @Nullable String prefix) {
634-
final RequestTarget res = DefaultRequestTarget.forClient(rawPath, prefix);
635-
if (res != null) {
636-
logger.info("forClient({}, {}) => path: {}, query: {}, fragment: {}", rawPath, prefix, res.path(),
637-
res.query(), res.fragment());
652+
private static RequestTargetWithRawPath forClient(String rawPath, @Nullable String prefix) {
653+
final RequestTarget target = DefaultRequestTarget.forClient(rawPath, prefix);
654+
if (target != null) {
655+
logger.info("forClient({}, {}) => path: {}, query: {}, fragment: {}",
656+
rawPath, prefix, target.path(),
657+
target.query(), target.fragment());
638658
} else {
639659
logger.info("forClient({}, {}) => null", rawPath, prefix);
640660
}
641-
return res;
661+
return new RequestTargetWithRawPath(null, target);
642662
}
643663

644664
private static String toAbsolutePath(String pattern) {

0 commit comments

Comments
 (0)