-
Notifications
You must be signed in to change notification settings - Fork 183
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
BlockingStreamingHttpService
: drop trailers if users didn't create any
#3151
Changes from all commits
39658f9
fd4b919
4f4134b
25ef79f
da746cd
3a27b66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -245,7 +245,7 @@ public <T> HttpRequest payloadBody(final T pojo, final HttpSerializer2<T> serial | |
public HttpHeaders trailers() { | ||
if (trailers == null) { | ||
trailers = original.payloadHolder().headersFactory().newTrailers(); | ||
original.transform(this); | ||
original.transform(this); // Invoke "transform" to set PayloadInfo.mayHaveTrailers() flag | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seemed odd to me and was one thing I had trouble grokking when I was trying to track this down. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree 😞 |
||
} | ||
return trailers; | ||
} | ||
|
@@ -277,6 +277,9 @@ public StreamingHttpRequest toStreamingRequest() { | |
@Nullable | ||
final Publisher<Object> payload; | ||
if (trailers != null) { | ||
// We can not drop empty Trailers here bcz users could do type conversion intermediately, while still | ||
// referencing the original HttpHeaders object from an aggregated type and keep using it to add trailers | ||
// before sending the message or converting it back to an aggregated one. | ||
payload = emptyPayloadBody ? from(trailers) : from(payloadBody, trailers); | ||
} else { | ||
payload = emptyPayloadBody ? null : from(payloadBody); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -16,19 +16,21 @@ | |||||||||||||||||||||||||||||||||
package io.servicetalk.http.api; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
import io.servicetalk.buffer.api.Buffer; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.buffer.api.BufferAllocator; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.concurrent.Cancellable; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.concurrent.PublisherSource.Subscriber; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.concurrent.PublisherSource.Subscription; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.concurrent.SingleSource; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.concurrent.api.Executor; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.concurrent.api.ExecutorExtension; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.concurrent.api.Publisher; | ||||||||||||||||||||||||||||||||||
import io.servicetalk.oio.api.PayloadWriter; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
import org.junit.jupiter.api.BeforeEach; | ||||||||||||||||||||||||||||||||||
import org.junit.jupiter.api.Test; | ||||||||||||||||||||||||||||||||||
import org.junit.jupiter.api.extension.ExtendWith; | ||||||||||||||||||||||||||||||||||
import org.junit.jupiter.api.extension.RegisterExtension; | ||||||||||||||||||||||||||||||||||
import org.junit.jupiter.params.ParameterizedTest; | ||||||||||||||||||||||||||||||||||
import org.junit.jupiter.params.provider.ValueSource; | ||||||||||||||||||||||||||||||||||
import org.mockito.Mock; | ||||||||||||||||||||||||||||||||||
import org.mockito.junit.jupiter.MockitoExtension; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
@@ -63,8 +65,10 @@ | |||||||||||||||||||||||||||||||||
import static java.nio.charset.StandardCharsets.UTF_8; | ||||||||||||||||||||||||||||||||||
import static java.util.function.Function.identity; | ||||||||||||||||||||||||||||||||||
import static org.hamcrest.MatcherAssert.assertThat; | ||||||||||||||||||||||||||||||||||
import static org.hamcrest.Matchers.containsInAnyOrder; | ||||||||||||||||||||||||||||||||||
import static org.hamcrest.Matchers.instanceOf; | ||||||||||||||||||||||||||||||||||
import static org.hamcrest.Matchers.is; | ||||||||||||||||||||||||||||||||||
import static org.hamcrest.Matchers.not; | ||||||||||||||||||||||||||||||||||
import static org.hamcrest.Matchers.notNullValue; | ||||||||||||||||||||||||||||||||||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||||||||||||||||||||||||||||||||||
import static org.junit.jupiter.api.Assertions.fail; | ||||||||||||||||||||||||||||||||||
|
@@ -78,7 +82,8 @@ class BlockingStreamingToStreamingServiceTest { | |||||||||||||||||||||||||||||||||
private static final String HELLO_WORLD = "Hello\nWorld\n"; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@RegisterExtension | ||||||||||||||||||||||||||||||||||
final ExecutorExtension<Executor> executorExtension = ExecutorExtension.withCachedExecutor(); | ||||||||||||||||||||||||||||||||||
static final ExecutorExtension<Executor> executorExtension = ExecutorExtension.withCachedExecutor() | ||||||||||||||||||||||||||||||||||
.setClassLevel(true); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@Mock | ||||||||||||||||||||||||||||||||||
private HttpExecutionContext mockExecutionCtx; | ||||||||||||||||||||||||||||||||||
|
@@ -93,14 +98,26 @@ void setup() { | |||||||||||||||||||||||||||||||||
mockCtx = new TestHttpServiceContext(DefaultHttpHeadersFactory.INSTANCE, reqRespFactory, mockExecutionCtx); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@Test | ||||||||||||||||||||||||||||||||||
void defaultResponseStatusNoPayload() throws Exception { | ||||||||||||||||||||||||||||||||||
BlockingStreamingHttpService syncService = (ctx, request, response) -> response.sendMetaData().close(); | ||||||||||||||||||||||||||||||||||
@ParameterizedTest(name = "{displayName} [{index}] withEmptyTrailers={0}") | ||||||||||||||||||||||||||||||||||
@ValueSource(booleans = {false, true}) | ||||||||||||||||||||||||||||||||||
void defaultResponseStatusNoPayload(boolean withEmptyTrailers) throws Exception { | ||||||||||||||||||||||||||||||||||
BlockingStreamingHttpService syncService = (ctx, request, response) -> { | ||||||||||||||||||||||||||||||||||
HttpPayloadWriter<Buffer> writer = response.sendMetaData(); | ||||||||||||||||||||||||||||||||||
if (withEmptyTrailers) { | ||||||||||||||||||||||||||||||||||
writer.trailers(); // accessing trailers before close should preserve trailers in message body | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the use-case of empty trailers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. Looking at it with a fresh morning look. My original thinking was that if users didn't touch trailers at all, then it's absolutely safe to drop them. But if they did, they could do something like: HttpHeaders trailers = writer.trailers();
....
trailers.add(name, value); However, if they did that before Another thing is how we treat trailers in all other places:
servicetalk/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequest.java Lines 277 to 283 in 6fc7987
Lines 340 to 348 in 6fc7987
Those account for the case when users can grab a reference to trailers, then convert the response and still use that reference to add trailers. Something like: HttpResponse aggregatedResponse = ...;
HttpHeaders trailers = aggregatedResponse.trailers();
StreamingHttpResponse streamingResponse = aggregatedResponse.toStreamingResponse();
trailers.add(name, value);
return Single.succeeded(streamingResponse); However, now I see that The only real reason to keep it as a null check instead of "null or empty" is to keep it consistent with the above 2 cases. WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think keeping the cases consistent makes sense but if there is a way to simplify I would prefer that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for starting in a consistent way and if we feel different, we can update them all together There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the explanation, makes sense to me to keep it consistent for now 👍 |
||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
writer.close(); | ||||||||||||||||||||||||||||||||||
writer.trailers(); // accessing trailers after close should not modify output | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
List<Object> response = invokeService(syncService, reqRespFactory.get("/")); | ||||||||||||||||||||||||||||||||||
assertMetaData(OK, response); | ||||||||||||||||||||||||||||||||||
assertPayloadBody("", response, false); | ||||||||||||||||||||||||||||||||||
assertEmptyTrailers(response); | ||||||||||||||||||||||||||||||||||
if (withEmptyTrailers) { | ||||||||||||||||||||||||||||||||||
assertEmptyTrailers(response); | ||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
assertNoTrailers(response); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@Test | ||||||||||||||||||||||||||||||||||
|
@@ -111,7 +128,7 @@ void customResponseStatusNoPayload() throws Exception { | |||||||||||||||||||||||||||||||||
List<Object> response = invokeService(syncService, reqRespFactory.get("/")); | ||||||||||||||||||||||||||||||||||
assertMetaData(NO_CONTENT, response); | ||||||||||||||||||||||||||||||||||
assertPayloadBody("", response, false); | ||||||||||||||||||||||||||||||||||
assertEmptyTrailers(response); | ||||||||||||||||||||||||||||||||||
assertNoTrailers(response); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@Test | ||||||||||||||||||||||||||||||||||
|
@@ -127,24 +144,34 @@ void receivePayloadBody() throws Exception { | |||||||||||||||||||||||||||||||||
.payloadBody(from("Hello\n", "World\n"), appSerializerUtf8FixLen())); | ||||||||||||||||||||||||||||||||||
assertMetaData(OK, response); | ||||||||||||||||||||||||||||||||||
assertPayloadBody("", response, true); | ||||||||||||||||||||||||||||||||||
assertEmptyTrailers(response); | ||||||||||||||||||||||||||||||||||
assertNoTrailers(response); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
assertThat(receivedPayload.toString(), is(HELLO_WORLD)); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@Test | ||||||||||||||||||||||||||||||||||
void respondWithPayloadBody() throws Exception { | ||||||||||||||||||||||||||||||||||
@ParameterizedTest(name = "{displayName} [{index}] withEmptyTrailers={0}") | ||||||||||||||||||||||||||||||||||
@ValueSource(booleans = {false, true}) | ||||||||||||||||||||||||||||||||||
void respondWithPayloadBody(boolean withEmptyTrailers) throws Exception { | ||||||||||||||||||||||||||||||||||
BlockingStreamingHttpService syncService = (ctx, request, response) -> { | ||||||||||||||||||||||||||||||||||
try (PayloadWriter<Buffer> pw = response.sendMetaData()) { | ||||||||||||||||||||||||||||||||||
pw.write(ctx.executionContext().bufferAllocator().fromAscii("Hello\n")); | ||||||||||||||||||||||||||||||||||
pw.write(ctx.executionContext().bufferAllocator().fromAscii("World\n")); | ||||||||||||||||||||||||||||||||||
BufferAllocator alloc = ctx.executionContext().bufferAllocator(); | ||||||||||||||||||||||||||||||||||
HttpPayloadWriter<Buffer> writer = response.sendMetaData(); | ||||||||||||||||||||||||||||||||||
writer.write(alloc.fromAscii("Hello\n")); | ||||||||||||||||||||||||||||||||||
if (withEmptyTrailers) { | ||||||||||||||||||||||||||||||||||
writer.trailers(); // accessing trailers before close should preserve trailers in message body | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
writer.write(alloc.fromAscii("World\n")); | ||||||||||||||||||||||||||||||||||
writer.close(); | ||||||||||||||||||||||||||||||||||
writer.trailers(); // accessing trailers after close should not modify output | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
List<Object> response = invokeService(syncService, reqRespFactory.get("/")); | ||||||||||||||||||||||||||||||||||
assertMetaData(OK, response); | ||||||||||||||||||||||||||||||||||
assertPayloadBody(HELLO_WORLD, response, false); | ||||||||||||||||||||||||||||||||||
assertEmptyTrailers(response); | ||||||||||||||||||||||||||||||||||
if (withEmptyTrailers) { | ||||||||||||||||||||||||||||||||||
assertEmptyTrailers(response); | ||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
assertNoTrailers(response); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@Test | ||||||||||||||||||||||||||||||||||
|
@@ -533,6 +560,10 @@ private static void assertPayloadBody(String expectedPayloadBody, List<Object> r | |||||||||||||||||||||||||||||||||
assertThat(payloadBody, is(expectedPayloadBody)); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
private static void assertNoTrailers(List<Object> response) { | ||||||||||||||||||||||||||||||||||
assertThat(response, not(containsInAnyOrder(instanceOf(HttpHeaders.class)))); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
private static void assertEmptyTrailers(List<Object> response) { | ||||||||||||||||||||||||||||||||||
HttpHeaders trailers = (HttpHeaders) response.get(response.size() - 1); | ||||||||||||||||||||||||||||||||||
assertThat(trailers, is(notNullValue())); | ||||||||||||||||||||||||||||||||||
|
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.
A "getter" method that also creates seems off to me. IDK if there is a better naming scheme where
trailers0
is basically justtrailers
andtrailers
isgetOrCreateTrailers
.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 "lazy get" 😄
We use the same trick in
AbstractHttpMetaData.context()
. When API is written in stone but we need to defer action.From users point of view it will be non-visible change.