@@ -4,6 +4,8 @@ import datadog.trace.agent.test.asserts.TraceAssert
44import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions
55import datadog.trace.bootstrap.instrumentation.api.Tags
66import datadog.trace.instrumentation.servlet3.AsyncDispatcherDecorator
7+ import okhttp3.MediaType
8+ import okhttp3.RequestBody
79import org.eclipse.jetty.continuation.Continuation
810import org.eclipse.jetty.continuation.ContinuationSupport
911import org.eclipse.jetty.server.Request
@@ -18,13 +20,46 @@ import java.util.concurrent.ExecutorService
1820import java.util.concurrent.Executors
1921
2022import static test.TestHandler.handleRequest
23+ import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED
2124import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
2225import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.TIMEOUT
2326import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.TIMEOUT_ERROR
2427import static datadog.trace.instrumentation.servlet3.TestServlet3.SERVLET_TIMEOUT
2528
2629abstract class JettyContinuationHandlerTest extends Jetty9Test {
2730
31+ def ' form requests survive continuation redispatch without HTTP retries' () {
32+ setup :
33+ def noRetryClient = client. newBuilder(). retryOnConnectionFailure(false ). build()
34+ int attempts = 100
35+
36+ when :
37+ attempts. times {
38+ def formRequest = request(BODY_URLENCODED , ' POST' ,
39+ RequestBody . create(MediaType . get(' application/x-www-form-urlencoded' ), ' a=x' )). build()
40+ try (def response = noRetryClient. newCall(formRequest). execute()) {
41+ assert response. code() == 200
42+ assert response. body(). string() == ' [a:[x]]'
43+ }
44+ }
45+ TEST_WRITER . waitForTraces(attempts)
46+
47+ then :
48+ TEST_WRITER . size() == attempts
49+ TEST_WRITER . every {
50+ trace ->
51+ def root = trace. find {
52+ it. parentId == 0
53+ }
54+ trace. size() == 3 &&
55+ root. getTag(' http.status_code' ) == 200 &&
56+ root. getTag(' request.body.converted' ) == ' [a:[x]]' &&
57+ trace. any {
58+ it. operationName == ' servlet.dispatch'
59+ }
60+ }
61+ }
62+
2863 @Override
2964 AbstractHandler handler () {
3065 def ret = new SessionHandler ()
@@ -48,6 +83,16 @@ abstract class JettyContinuationHandlerTest extends Jetty9Test {
4883 // this happens in the /exception endpoint
4984 if (! request. getAttribute(' javax.servlet.error.status_code' )) {
5085 if (continuation. initial) {
86+ if (request. contentType?. startsWith(' application/x-www-form-urlencoded' )) {
87+ // Parse the form before suspending the request. When the client waits for `HTTP 100 Continue`
88+ // before sending the body, parsing after resume can overlap with Jetty 9.0.0's pending connection read
89+ // and cause ReadPendingException.
90+ request. parameterMap
91+ if (response. committed) {
92+ baseRequest. handled = true
93+ return
94+ }
95+ }
5196 continuation. suspend()
5297 executorService. execute {
5398 continuation. resume()
0 commit comments