Skip to content

Commit aa4b789

Browse files
authored
Fix to accept valid ETag values for watch API (#654)
Motivation: - `If-None-Match` is often used with `ETag`. Let centraldogma also allow valid `ETag`. Modifications: - Parse `ETag` to interpretable `Revision` parameter. Result: - Closes #415
1 parent d596a34 commit aa4b789

File tree

2 files changed

+72
-21
lines changed

2 files changed

+72
-21
lines changed

server/src/main/java/com/linecorp/centraldogma/server/internal/api/converter/WatchRequestConverter.java

+23-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import javax.annotation.Nullable;
2929

30+
import com.google.common.annotations.VisibleForTesting;
3031
import com.google.common.base.MoreObjects;
3132
import com.google.common.base.Splitter;
3233

@@ -65,7 +66,7 @@ public WatchRequest convertRequest(
6566
return null;
6667
}
6768

68-
final Revision lastKnownRevision = new Revision(ifNoneMatch);
69+
final Revision lastKnownRevision = new Revision(extractRevision(ifNoneMatch));
6970
final String prefer = request.headers().get(HttpHeaderNames.PREFER);
7071
final long timeoutMillis;
7172
final boolean notifyEntryNotFound;
@@ -81,6 +82,27 @@ public WatchRequest convertRequest(
8182
return new WatchRequest(lastKnownRevision, timeoutMillis, notifyEntryNotFound);
8283
}
8384

85+
@VisibleForTesting
86+
String extractRevision(String ifNoneMatch) {
87+
final int length = ifNoneMatch.length();
88+
89+
// Three below cases are valid. See https://github.com/line/centraldogma/issues/415
90+
// - <revision> (for backward compatibility)
91+
// - "<revision>"
92+
// - W/"<revision>"
93+
if (length > 2 && ifNoneMatch.charAt(0) == '"' &&
94+
ifNoneMatch.charAt(length - 1) == '"') {
95+
return ifNoneMatch.substring(1, length - 1);
96+
}
97+
98+
if (length > 4 && ifNoneMatch.startsWith("W/\"") &&
99+
ifNoneMatch.charAt(length - 1) == '"') {
100+
return ifNoneMatch.substring(3, length - 1);
101+
}
102+
103+
return ifNoneMatch;
104+
}
105+
84106
// TODO(minwoox) Use https://github.com/line/armeria/issues/1835
85107
private static Map<String, String> extract(String preferHeader) {
86108
final Iterable<String> preferences = preferenceSplitter.split(preferHeader);

server/src/test/java/com/linecorp/centraldogma/server/internal/api/converter/WatchRequestConverterTest.java

+49-20
Original file line numberDiff line numberDiff line change
@@ -17,47 +17,76 @@
1717
package com.linecorp.centraldogma.server.internal.api.converter;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20-
import static org.mockito.Mockito.mock;
21-
import static org.mockito.Mockito.when;
2220

2321
import javax.annotation.Nullable;
2422

2523
import org.junit.jupiter.api.Test;
2624

2725
import com.linecorp.armeria.common.AggregatedHttpRequest;
28-
import com.linecorp.armeria.common.HttpHeaderNames;
2926
import com.linecorp.armeria.common.HttpMethod;
3027
import com.linecorp.armeria.common.RequestHeaders;
3128
import com.linecorp.armeria.server.ServiceRequestContext;
32-
import com.linecorp.centraldogma.common.Revision;
3329
import com.linecorp.centraldogma.server.internal.api.converter.WatchRequestConverter.WatchRequest;
3430

3531
class WatchRequestConverterTest {
3632

3733
private static final WatchRequestConverter converter = new WatchRequestConverter();
3834

3935
@Test
40-
void convertWatchRequest() throws Exception {
41-
final ServiceRequestContext ctx = mock(ServiceRequestContext.class);
42-
final AggregatedHttpRequest request = mock(AggregatedHttpRequest.class);
43-
final RequestHeaders headers = RequestHeaders.of(HttpMethod.GET, "/",
44-
HttpHeaderNames.IF_NONE_MATCH, "-1",
45-
HttpHeaderNames.PREFER, "wait=10");
46-
when(request.headers()).thenReturn(headers);
36+
void extractValidRevision() {
37+
final String firstIfNoneMatch = "-1";
38+
final String firstRevision = converter.extractRevision(firstIfNoneMatch);
39+
assertThat(firstRevision).isEqualTo(firstIfNoneMatch);
4740

48-
final WatchRequest watchRequest = convert(ctx, request);
49-
assertThat(watchRequest).isNotNull();
50-
assertThat(watchRequest.lastKnownRevision()).isEqualTo(Revision.HEAD);
51-
assertThat(watchRequest.timeoutMillis()).isEqualTo(10000); // 10 seconds
41+
final String secondIfNoneMatch = "\"-1\"";
42+
final String secondRevision = converter.extractRevision(secondIfNoneMatch);
43+
assertThat(secondRevision).isEqualTo("-1");
44+
45+
final String thirdIfNoneMatch = "W/\"-1\"";
46+
final String thirdRevision = converter.extractRevision(thirdIfNoneMatch);
47+
assertThat(thirdRevision).isEqualTo("-1");
48+
}
49+
50+
@Test
51+
void extractInvalidRevision() {
52+
final String firstIfNoneMatch = "w/\"-1\"";
53+
final String firstRevision = converter.extractRevision(firstIfNoneMatch);
54+
assertThat(firstRevision).isEqualTo(firstIfNoneMatch);
55+
56+
final String secondIfNoneMatch = "W\"-1\"";
57+
final String secondRevision = converter.extractRevision(secondIfNoneMatch);
58+
assertThat(secondRevision).isEqualTo(secondIfNoneMatch);
59+
60+
final String thirdIfNoneMatch = "/\"-1\"";
61+
final String thirdRevision = converter.extractRevision(thirdIfNoneMatch);
62+
assertThat(thirdRevision).isEqualTo(thirdIfNoneMatch);
63+
64+
final String fourthIfNoneMatch = "-1\"";
65+
final String fourthRevision = converter.extractRevision(fourthIfNoneMatch);
66+
assertThat(fourthRevision).isEqualTo(fourthIfNoneMatch);
67+
68+
final String fifthIfNoneMatch = "\"-1";
69+
final String fifthRevision = converter.extractRevision(fifthIfNoneMatch);
70+
assertThat(fifthRevision).isEqualTo(fifthIfNoneMatch);
71+
72+
final String sixthIfNoneMatch = "W/\"";
73+
final String sixthRevision = converter.extractRevision(sixthIfNoneMatch);
74+
assertThat(sixthRevision).isEqualTo(sixthIfNoneMatch);
75+
76+
final String seventhIfNoneMatch = "\"";
77+
final String seventhRevision = converter.extractRevision(seventhIfNoneMatch);
78+
assertThat(seventhRevision).isEqualTo(seventhIfNoneMatch);
79+
80+
final String eighthIfNoneMatch = "\"\"";
81+
final String eighthRevision = converter.extractRevision(eighthIfNoneMatch);
82+
assertThat(eighthRevision).isEqualTo(eighthIfNoneMatch);
5283
}
5384

5485
@Test
5586
void emptyHeader() throws Exception {
56-
final ServiceRequestContext ctx = mock(ServiceRequestContext.class);
57-
final AggregatedHttpRequest request = mock(AggregatedHttpRequest.class);
5887
final RequestHeaders headers = RequestHeaders.of(HttpMethod.GET, "/");
59-
60-
when(request.headers()).thenReturn(headers);
88+
final AggregatedHttpRequest request = AggregatedHttpRequest.of(headers);
89+
final ServiceRequestContext ctx = ServiceRequestContext.of(request.toHttpRequest());
6190

6291
final WatchRequest watchRequest = convert(ctx, request);
6392
assertThat(watchRequest).isNull();
@@ -66,6 +95,6 @@ void emptyHeader() throws Exception {
6695
@Nullable
6796
private static WatchRequest convert(
6897
ServiceRequestContext ctx, AggregatedHttpRequest request) throws Exception {
69-
return (WatchRequest) converter.convertRequest(ctx, request, null, null);
98+
return converter.convertRequest(ctx, request, null, null);
7099
}
71100
}

0 commit comments

Comments
 (0)