Skip to content

Commit 5fc0969

Browse files
authored
Keep Netty 4.1 server spans open until streaming responses complete (#11941)
test(netty-4.1): Add pipelining test fix(netty-4.1): pipelined response context tracking Disable Netty server tracing on context queue overflow Fix Netty 4.1 response context handling Track server request contexts per pipelined response, snapshot only the Accept header, and keep AppSec response-block state channel-level so late streaming chunks are dropped until close. Add Netty chunked response span timing tests Fix Netty 4.1 server span completion for streaming responses Keep server spans open until terminal response writes, finish incomplete responses on channel close, and use queued request contexts to avoid keep-alive/pipelining misattribution. Cover chunked, bodyless, HEAD, CONNECT, interim, and WebSocket upgrade response cases. Fix Netty server span lifecycle for streaming responses Keep Netty 4.1 server spans open until terminal response writes while preserving request-ended callback timing for AppSec/IAST. Track one-shot beforeFinish execution separately from span finish so streaming spans get correct duration without dropping request-end security tags. Fix Netty 4.1 server span edge cases from PR review - Defer beforeFinish until the response actually completes. - Finish close-delimited responses without error. - Guard Content-Length parsing. - Skip AppSec response analysis for all interim 1xx responses in MaybeBlockResponseHandler, not just 100 Continue. Add a regression test for close-delimited responses. Restore request-ended callbacks for terminal Netty responses Resolved: terminal responses no longer call `beforeFinish(...)` immediately after `ctx.write(...)`. The write-promise listener is now the only normal terminal completion path, so `finishSpan(...)` runs after the write completes and invokes `beforeFinish(...)` there. The exception path is also guarded with `writePromise.isDone()`: if the terminal write promise has already completed, the listener has already handled finishing, so the catch block just rethrows and avoids a second finish/decorator pass. Changed HttpServerResponseTracingHandler.java so unknown-length responses are marked close-delimited before `LastHttpContent` can make them terminal. Close-delimited contexts now wait for `channelInactive`. Added a regression in NettyChunkedResponseSpanTest.java for `DefaultFullHttpResponse` with no `Content-Length` and no chunked encoding. Restrict close-delimited response handling to HTTP/1.x only. The Netty HTTP/2 server path now caches when `Http2StreamFrameToHttpObjectCodec` is present and avoids marking no-length responses as close-delimited there. This keeps HTTP/2 responses dependent on `LastHttpContent`/END_STREAM, so an early stream close is still reported as an incomplete-response error. The Netty 4.1 websocket upgrade check is now value-based for status `101`, and the regression test for a distinct `HttpResponseStatus(101, ...)` passes. It now treats known-length responses as complete once the declared body bytes are successfully written, so closing the connection afterward no longer marks the span as an incomplete-response error. Chunked/incomplete known-length closes still error as expected. Tests passed for `NettyChunkedResponseSpanTest`; changes are not committed. Fix Netty IAST reporting on async response completion Reactivate the stored request context while finishing Netty server spans so IAST request-ended callbacks run with the request span active. This lets sink reports attach to the request span when response write promises complete asynchronously or on channel close. Add Netty regressions covering IAST request-ended callbacks for known-length full responses and delayed write-promise completion. Preserve Netty HTTP/2 stream contexts Fallback to the channel context mirror when no queued ServerRequestContext exists, so HTTP/2 multiplex child-channel responses still decorate and finish propagated server spans. Add regression coverage for the no-queue context-mirror path and document why CONTEXT_ATTRIBUTE_KEY is maintained. Fix deferred AppSec block responses for Netty pipelining Avoid logging dropped Netty outbound messages Use computed Netty 4.1 response status code Use single-thread Netty event loop in tests Merge branch 'ygree/fix-netty41-http11-pipelining-context-queue' into ygree/fix-netty41-chunked-tracing Avoid synthetic accessors in Netty blocking response handler Fix Netty block function pipelining deferral Fix Netty pipelined block response ordering Defer blocked responses until the active HTTP/1.1 response is complete, including chunked/streaming responses that finish on LastHttpContent. Handle header-only responses such as HEAD, 204, 205, 304, and explicit Content-Length: 0 without requiring an application LastHttpContent. Also keep interim 1xx responses from completing the server span and add coverage for chunked, header-only, HEAD, interim, and malformed Content-Length cases. Fix Netty HTTP/2 response AppSec context handling Fall back to the mirrored channel context when HTTP/2 stream channels have no request context queue. Ignore informational responses until the final response and add regression coverage for response blocking. Fix Netty AppSec blocked response handling Merge branch 'ygree/fix-netty41-http11-pipelining-context-queue' into ygree/fix-netty41-chunked-tracing Merge branch 'master' into ygree/fix-netty41-http11-pipelining-context-queue Merge branch 'ygree/fix-netty41-http11-pipelining-context-queue' into ygree/fix-netty41-chunked-tracing Align request-context completion with Netty’s message framing contract. This removes HEAD-specific state, header parsing, and synthetic outbound messages while making pipelined response attribution follow Netty’s actual response boundaries. Reuse Netty blocking handler for deferred pipelined requests Mark channels when a request block is committed so later pipelined requests are forwarded to the existing blocking handler and discarded. Serialize response-function handler installation on the event loop and clean up partially installed handlers on failure. Fix Netty blocking response scheduling races Snapshot request metadata before scheduling blocking responses instead of retaining and replaying reference-counted HTTP requests. Reject scheduled blocking responses when their server request context has already completed, preventing stale commits from blocking keep-alive connections. Reuse Netty request context queues for keep-alive traffic Reuse Netty request context queues for keep-alive traffic Merge branch 'ygree/fix-netty41-http11-pipelining-context-queue' into ygree/fix-netty41-chunked-tracing Align Netty response completion with LastHttpContent Finish server spans and dequeue pipelined request contexts only when Netty writes LastHttpContent. Use the terminal write promise so asynchronous failures retain error reporting and request context. Remove Content-Length, bodyless-response, request-method, and close-delimited completion inference, along with synthetic terminators and the HTTP/2 marker used by that model. Preserve channel-close cleanup for responses that never reach terminal content. Update pipelining, AppSec, HTTP/2, framing, and asynchronous write tests for the terminal-content contract. Merge branch 'master' into ygree/fix-netty41-http11-pipelining-context-queue Merge branch 'ygree/fix-netty41-http11-pipelining-context-queue' into ygree/fix-netty41-chunked-tracing Raise agent jar size budget The agent jar exceeds the previous budget by about 1 KiB. Raise the budget to the next 16 KiB boundary to pass the verification check. Merge branch 'master' into ygree/fix-netty41-chunked-tracing Wait for Netty server spans before closing test sockets Merge branch 'master' into ygree/fix-netty41-chunked-tracing Co-authored-by: yury.gribkov <yury.gribkov@datadoghq.com>
1 parent a92b3a6 commit 5fc0969

8 files changed

Lines changed: 1362 additions & 225 deletions

File tree

dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerRequestTracingHandler.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io.netty.channel.ChannelInboundHandlerAdapter;
1616
import io.netty.handler.codec.http.HttpHeaders;
1717
import io.netty.handler.codec.http.HttpRequest;
18+
import java.util.Deque;
1819

1920
@ChannelHandler.Sharable
2021
public class HttpServerRequestTracingHandler extends ChannelInboundHandlerAdapter {
@@ -95,9 +96,43 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
9596
super.channelInactive(ctx);
9697
} finally {
9798
try {
98-
ServerRequestContext.closeAll(ctx.channel());
99+
final Deque<ServerRequestContext> storedContexts =
100+
ServerRequestContext.removeAll(ctx.channel());
101+
if (storedContexts != null) {
102+
ServerRequestContext storedContext;
103+
while ((storedContext = storedContexts.pollFirst()) != null) {
104+
if (storedContext.isResponseStarted()) {
105+
finishSpanOnChannelClose(storedContext);
106+
} else {
107+
publishSpanOnChannelClose(storedContext.tracingContext());
108+
}
109+
}
110+
}
99111
} catch (final Throwable ignored) {
100112
}
101113
}
102114
}
115+
116+
private static void finishSpanOnChannelClose(final ServerRequestContext serverContext) {
117+
final Context storedContext = serverContext.tracingContext();
118+
final AgentSpan span = AgentSpan.fromContext(storedContext);
119+
if (span == null) {
120+
return;
121+
}
122+
try (final ContextScope ignored = storedContext.attach()) {
123+
if (!serverContext.isBeforeFinishCalled()) {
124+
serverContext.markBeforeFinishCalled();
125+
DECORATE.beforeFinish(storedContext);
126+
}
127+
span.finish();
128+
}
129+
}
130+
131+
private static void publishSpanOnChannelClose(final Context storedContext) {
132+
final AgentSpan span = AgentSpan.fromContext(storedContext);
133+
if (span != null && span.phasedFinish()) {
134+
// At this point we can just publish this span to avoid losing the rest of the trace.
135+
span.publish();
136+
}
137+
}
103138
}

dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
import io.netty.channel.ChannelOutboundHandlerAdapter;
1515
import io.netty.channel.ChannelPromise;
1616
import io.netty.handler.codec.http.HttpHeaderNames;
17+
import io.netty.handler.codec.http.HttpHeaderValues;
1718
import io.netty.handler.codec.http.HttpResponse;
1819
import io.netty.handler.codec.http.HttpResponseStatus;
1920
import io.netty.handler.codec.http.LastHttpContent;
21+
import io.netty.util.concurrent.Future;
2022

2123
@ChannelHandler.Sharable
2224
public class HttpServerResponseTracingHandler extends ChannelOutboundHandlerAdapter {
@@ -25,7 +27,8 @@ public class HttpServerResponseTracingHandler extends ChannelOutboundHandlerAdap
2527
@Override
2628
public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise prm) {
2729
final boolean isResponse = msg instanceof HttpResponse;
28-
if (!isResponse && !(msg instanceof LastHttpContent)) {
30+
final boolean isLastContent = msg instanceof LastHttpContent;
31+
if (!isResponse && !isLastContent) {
2932
ctx.write(msg, prm);
3033
return;
3134
}
@@ -50,43 +53,99 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
5053
return;
5154
}
5255

53-
try (final ContextScope scope = storedContext.attach()) {
56+
try (final ContextScope ignored = storedContext.attach()) {
5457
final HttpResponse response = isResponse ? (HttpResponse) msg : null;
55-
final boolean responseComplete = msg instanceof LastHttpContent;
56-
58+
final boolean websocketUpgrade = response != null && isWebsocketUpgrade(response);
59+
final boolean informationalResponse =
60+
response != null && isInformationalResponse(response) && !websocketUpgrade;
61+
final boolean finishResponseOnWrite = isLastContent && !informationalResponse;
62+
final ChannelPromise writePromise =
63+
finishResponseOnWrite && prm.isVoid() ? ctx.newPromise() : prm;
5764
try {
58-
ctx.write(msg, prm);
59-
} catch (final Throwable throwable) {
60-
DECORATE.onError(span, throwable);
61-
span.setHttpStatusCode(500);
62-
span.finish(); // Finish the span manually since finishSpanOnClose was false
63-
removeServerContext(ctx, serverContext);
64-
throw throwable;
65-
}
66-
if (response != null) {
67-
final boolean isWebsocketUpgrade =
68-
response.status() == HttpResponseStatus.SWITCHING_PROTOCOLS
69-
&& "websocket".equals(response.headers().get(HttpHeaderNames.UPGRADE));
70-
if (isWebsocketUpgrade) {
71-
ctx.channel()
72-
.attr(WEBSOCKET_SENDER_HANDLER_CONTEXT)
73-
.set(new HandlerContext.Sender(span, ctx.channel().id().asShortText()));
65+
if (response != null && !informationalResponse) {
66+
onResponse(ctx, span, serverContext, response, websocketUpgrade);
7467
}
75-
if (isInformational(response) && !isWebsocketUpgrade) {
76-
return;
68+
if (finishResponseOnWrite) {
69+
removeServerContext(ctx, serverContext);
70+
writePromise.addListener(
71+
future -> finishSpan(serverContext, storedContext, span, future));
7772
}
78-
if (serverContext != null) {
79-
serverContext.markResponseStarted();
73+
ctx.write(msg, writePromise);
74+
if (finishResponseOnWrite && (!writePromise.isDone() || writePromise.isSuccess())) {
75+
final ServerRequestContext nextResponse =
76+
ServerRequestContext.nextResponse(ctx.channel());
77+
BlockingResponseHandler.maybeWriteDeferredBlockResponse(ctx, nextResponse);
8078
}
81-
DECORATE.onResponse(span, response);
79+
} catch (final Throwable throwable) {
80+
if (!finishResponseOnWrite || !writePromise.isDone()) {
81+
DECORATE.onError(span, throwable);
82+
span.setHttpStatusCode(500);
83+
if (!finishResponseOnWrite) {
84+
removeServerContext(ctx, serverContext);
85+
}
86+
finishSpan(serverContext, storedContext, span);
87+
}
88+
throw throwable;
8289
}
83-
if (responseComplete) {
84-
DECORATE.beforeFinish(scope.context());
85-
span.finish(); // Finish the span manually since finishSpanOnClose was false
86-
removeServerContext(ctx, serverContext);
87-
final ServerRequestContext nextResponse = ServerRequestContext.nextResponse(ctx.channel());
88-
BlockingResponseHandler.maybeWriteDeferredBlockResponse(ctx, nextResponse);
90+
}
91+
}
92+
93+
private static void onResponse(
94+
final ChannelHandlerContext ctx,
95+
final AgentSpan span,
96+
final ServerRequestContext serverContext,
97+
final HttpResponse response,
98+
final boolean websocketUpgrade) {
99+
if (websocketUpgrade) {
100+
ctx.channel()
101+
.attr(WEBSOCKET_SENDER_HANDLER_CONTEXT)
102+
.set(new HandlerContext.Sender(span, ctx.channel().id().asShortText()));
103+
}
104+
DECORATE.onResponse(span, response);
105+
if (serverContext != null) {
106+
serverContext.markResponseStarted();
107+
}
108+
}
109+
110+
private static boolean isInformationalResponse(final HttpResponse response) {
111+
final int statusCode = response.status().code();
112+
return statusCode >= 100 && statusCode < 200;
113+
}
114+
115+
private static boolean isWebsocketUpgrade(final HttpResponse response) {
116+
return response.status().code() == HttpResponseStatus.SWITCHING_PROTOCOLS.code()
117+
&& response
118+
.headers()
119+
.containsValue(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET, true);
120+
}
121+
122+
private static void finishSpan(
123+
final ServerRequestContext serverContext,
124+
final Context storedContext,
125+
final AgentSpan span,
126+
final Future<?> future) {
127+
if (!future.isSuccess()) {
128+
DECORATE.onError(span, future.cause());
129+
span.setHttpStatusCode(500);
130+
}
131+
finishSpan(serverContext, storedContext, span);
132+
}
133+
134+
private static void finishSpan(
135+
final ServerRequestContext serverContext, final Context storedContext, final AgentSpan span) {
136+
try (final ContextScope ignored = storedContext.attach()) {
137+
beforeFinish(serverContext, storedContext);
138+
span.finish(); // Finish the span manually since finishSpanOnClose was false
139+
}
140+
}
141+
142+
private static void beforeFinish(
143+
final ServerRequestContext serverContext, final Context storedContext) {
144+
if (serverContext == null || !serverContext.isBeforeFinishCalled()) {
145+
if (serverContext != null) {
146+
serverContext.markBeforeFinishCalled();
89147
}
148+
DECORATE.beforeFinish(storedContext);
90149
}
91150
}
92151

@@ -98,9 +157,4 @@ private static void removeServerContext(
98157
ServerRequestContext.remove(ctx.channel(), serverContext);
99158
}
100159
}
101-
102-
private static boolean isInformational(final HttpResponse response) {
103-
final int statusCode = response.status().code();
104-
return statusCode >= 100 && statusCode < 200;
105-
}
106160
}

dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/MaybeBlockResponseHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) thr
7070
}
7171
HttpResponse origResponse = (HttpResponse) msg;
7272
int statusCode = origResponse.status().code();
73+
// Interim 1xx responses (e.g. 100 Continue, 103 Early Hints) precede the final response.
74+
// Analyzing one here would consume the one-shot response analysis before the final response is
75+
// written, so its status and headers would never be inspected. Switching Protocols (101) is
76+
// terminal, so it is still analyzed.
7377
if (statusCode >= 100
7478
&& statusCode < 200
7579
&& statusCode != HttpResponseStatus.SWITCHING_PROTOCOLS.code()) {

dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandlerTest.java

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
1414
import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
1515
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
16+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
1617
import static org.junit.jupiter.api.Assertions.assertNotNull;
1718
import static org.junit.jupiter.api.Assertions.assertNull;
1819
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -39,16 +40,22 @@
3940
class HttpServerResponseTracingHandlerTest extends AbstractInstrumentationTest {
4041

4142
@Test
42-
void finishesMirroredContextWhenRequestQueueIsAbsent() {
43+
void finishesMirroredContextOnLastContentWhenRequestQueueIsAbsent() {
4344
EmbeddedChannel channel = new EmbeddedChannel(HttpServerResponseTracingHandler.INSTANCE);
4445
AgentSpan span = startSpan("netty", "mirrored-http2-server");
4546
channel.attr(CONTEXT_ATTRIBUTE_KEY).set(span);
4647

47-
assertTrue(channel.writeOutbound(new DefaultFullHttpResponse(HTTP_1_1, OK)));
48+
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
49+
assertTrue(channel.writeOutbound(response));
4850

49-
FullHttpResponse response = channel.readOutbound();
50-
assertNotNull(response);
51-
response.release();
51+
assertSame(span, channel.attr(CONTEXT_ATTRIBUTE_KEY).get());
52+
HttpResponse forwarded = channel.readOutbound();
53+
assertSame(response, forwarded);
54+
ReferenceCountUtil.release(forwarded);
55+
56+
assertTrue(channel.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT));
57+
58+
ReferenceCountUtil.release(channel.readOutbound());
5259
assertNull(channel.attr(CONTEXT_ATTRIBUTE_KEY).get());
5360
channel.finishAndReleaseAll();
5461
assertTraces(trace(span().root().operationName("mirrored-http2-server")));
@@ -70,7 +77,7 @@ void forwardsLastContentBeforeFinalResponseWithoutCompletingContext() {
7077
}
7178

7279
@Test
73-
void headerOnlyResponseDoesNotCompleteContextBeforeLastContent() {
80+
void headerOnlyResponseWaitsForLastContent() {
7481
EmbeddedChannel channel = new EmbeddedChannel(HttpServerResponseTracingHandler.INSTANCE);
7582
AgentSpan span = startSpan("netty", "header-only-server");
7683
ServerRequestContext serverContext = ServerRequestContext.add(channel, span, null);
@@ -84,6 +91,7 @@ void headerOnlyResponseDoesNotCompleteContextBeforeLastContent() {
8491
HttpResponse forwarded = channel.readOutbound();
8592
assertSame(response, forwarded);
8693
ReferenceCountUtil.release(forwarded);
94+
assertNull(channel.readOutbound());
8795

8896
assertTrue(channel.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT));
8997

@@ -95,10 +103,12 @@ void headerOnlyResponseDoesNotCompleteContextBeforeLastContent() {
95103
}
96104

97105
@Test
98-
void rawFixedLengthBodyDoesNotCompleteContextBeforeLastContent() {
106+
void rawFixedLengthBodyWaitsForLastContent() {
99107
EmbeddedChannel channel = new EmbeddedChannel(HttpServerResponseTracingHandler.INSTANCE);
100108
AgentSpan span = startSpan("netty", "raw-body-server");
101109
ServerRequestContext serverContext = ServerRequestContext.add(channel, span, null);
110+
ServerRequestContext nextServerContext =
111+
ServerRequestContext.add(channel, Context.root(), null);
102112
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
103113
response.headers().set(CONTENT_LENGTH, 4);
104114

@@ -108,15 +118,34 @@ void rawFixedLengthBodyDoesNotCompleteContextBeforeLastContent() {
108118

109119
assertSame(serverContext, ServerRequestContext.nextResponse(channel));
110120
ReferenceCountUtil.release(channel.readOutbound());
121+
assertNull(channel.readOutbound());
111122

112123
assertTrue(channel.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT));
113124

114-
assertNull(ServerRequestContext.nextResponse(channel));
125+
assertSame(nextServerContext, ServerRequestContext.nextResponse(channel));
115126
ReferenceCountUtil.release(channel.readOutbound());
127+
ServerRequestContext.remove(channel, nextServerContext);
116128
channel.finishAndReleaseAll();
117129
assertTraces(trace(span().root().operationName("raw-body-server")));
118130
}
119131

132+
@Test
133+
void doesNotThrowOnMalformedContentLength() {
134+
EmbeddedChannel channel = new EmbeddedChannel(HttpServerResponseTracingHandler.INSTANCE);
135+
AgentSpan span = startSpan("netty", "malformed-content-length-server");
136+
ServerRequestContext.add(channel, span, null);
137+
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
138+
response.headers().set(CONTENT_LENGTH, "malformed");
139+
140+
assertDoesNotThrow(() -> assertTrue(channel.writeOutbound(response)));
141+
142+
FullHttpResponse forwarded = channel.readOutbound();
143+
assertSame(response, forwarded);
144+
forwarded.release();
145+
channel.finishAndReleaseAll();
146+
assertTraces(trace(span().root().operationName("malformed-content-length-server")));
147+
}
148+
120149
@Test
121150
void nettyEncoderRequiresLastContentBeforeNextKeepAliveResponse() {
122151
EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseEncoder());
@@ -159,4 +188,27 @@ void fullWebsocketUpgradeCompletesContextAndPreservesHandshakeSpan() {
159188
channel.finishAndReleaseAll();
160189
assertTraces(trace(span().root().operationName("websocket-handshake-server")));
161190
}
191+
192+
@Test
193+
void fullNonWebsocketUpgradeWaitsForFinalResponse() {
194+
EmbeddedChannel channel = new EmbeddedChannel(HttpServerResponseTracingHandler.INSTANCE);
195+
AgentSpan span = startSpan("netty", "h2c-upgrade-server");
196+
ServerRequestContext serverContext = ServerRequestContext.add(channel, span, null);
197+
FullHttpResponse upgradeResponse = new DefaultFullHttpResponse(HTTP_1_1, SWITCHING_PROTOCOLS);
198+
upgradeResponse.headers().set(UPGRADE, "h2c");
199+
200+
assertTrue(channel.writeOutbound(upgradeResponse));
201+
202+
assertSame(serverContext, ServerRequestContext.nextResponse(channel));
203+
assertNull(channel.attr(WEBSOCKET_SENDER_HANDLER_CONTEXT).get());
204+
ReferenceCountUtil.release(channel.readOutbound());
205+
206+
assertTrue(channel.writeOutbound(new DefaultFullHttpResponse(HTTP_1_1, OK)));
207+
208+
assertNull(ServerRequestContext.nextResponse(channel));
209+
ReferenceCountUtil.release(channel.readOutbound());
210+
channel.finishAndReleaseAll();
211+
212+
assertTraces(trace(span().root().operationName("h2c-upgrade-server")));
213+
}
162214
}

0 commit comments

Comments
 (0)