Skip to content

Commit 462a5a5

Browse files
committed
wip
Signed-off-by: sezen.leblay <[email protected]>
1 parent 8f0465e commit 462a5a5

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
2+
import datadog.trace.agent.test.base.HttpServerTest
3+
import datadog.trace.api.gateway.CallbackProvider
4+
import datadog.trace.api.gateway.Events
5+
import datadog.trace.api.gateway.RequestContext
6+
import datadog.trace.api.gateway.RequestContextSlot
7+
import datadog.trace.api.http.StoredBodySupplier
8+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
9+
import datadog.trace.instrumentation.servlet.BufferedWriterWrapper
10+
import datadog.trace.instrumentation.servlet3.Servlet31OutputStreamWrapper
11+
import groovy.servlet.AbstractHttpServlet
12+
import spock.lang.Shared
13+
14+
import javax.servlet.ServletOutputStream
15+
import javax.servlet.annotation.WebServlet
16+
import javax.servlet.http.HttpServletRequest
17+
import javax.servlet.http.HttpServletResponse
18+
import java.util.function.BiFunction
19+
20+
class Servlet31ResponseBodyInstrumentationTest extends TomcatServlet3Test {
21+
22+
@Shared
23+
CallbackProvider ig
24+
25+
def setupSpec() {
26+
// Set up AppSec callbacks to enable response body instrumentation
27+
ig = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC)
28+
Events<Object> events = Events.get()
29+
30+
// Register minimal callbacks needed for response body instrumentation
31+
ig.registerCallback(events.responseBodyStart(), { RequestContext ctx, StoredBodySupplier supplier ->
32+
// Simple callback that just acknowledges the response body start
33+
return null
34+
} as BiFunction<RequestContext, StoredBodySupplier, Void>)
35+
36+
ig.registerCallback(events.responseBodyDone(), { RequestContext ctx, StoredBodySupplier supplier ->
37+
// Simple callback that just acknowledges the response body end
38+
return datadog.trace.api.gateway.Flow.ResultFlow.empty()
39+
} as BiFunction<RequestContext, StoredBodySupplier, datadog.trace.api.gateway.Flow<Void>>)
40+
}
41+
42+
def cleanupSpec() {
43+
if (ig != null) {
44+
ig.reset()
45+
}
46+
}
47+
48+
@Override
49+
Class servlet() {
50+
ResponseBodyTestServlet
51+
}
52+
53+
@Override
54+
String getContext() {
55+
return "response-body-test"
56+
}
57+
58+
def "test getOutputStream is wrapped with Servlet31OutputStreamWrapper"() {
59+
setup:
60+
def request = request(HttpServerTest.ServerEndpoint.SUCCESS, "GET", null).build()
61+
62+
when:
63+
def response = client.newCall(request).execute()
64+
65+
then:
66+
response.code() == 200
67+
response.body().string() == "output-stream-response"
68+
69+
and:
70+
// Verify that the instrumentation was applied by checking for the wrapper header
71+
response.header("X-Stream-Wrapped") == "true"
72+
}
73+
74+
def "test getWriter is wrapped with BufferedWriterWrapper"() {
75+
setup:
76+
def request = request(HttpServerTest.ServerEndpoint.FORWARDED, "GET", null).build()
77+
78+
when:
79+
def response = client.newCall(request).execute()
80+
81+
then:
82+
response.code() == 200
83+
response.body().string() == "writer-response"
84+
85+
and:
86+
// Verify that the instrumentation was applied by checking for the wrapper header
87+
response.header("X-Writer-Wrapped") == "true"
88+
}
89+
90+
def "test both getOutputStream and getWriter handling"() {
91+
setup:
92+
def request = request(HttpServerTest.ServerEndpoint.QUERY_PARAM, "GET", null).build()
93+
94+
when:
95+
def response = client.newCall(request).execute()
96+
97+
then:
98+
response.code() == 200
99+
response.body().string() == "both-response"
100+
101+
and:
102+
// Only one should be wrapped since they're mutually exclusive in servlet spec
103+
(response.header("X-Stream-Wrapped") == "true") || (response.header("X-Writer-Wrapped") == "true")
104+
}
105+
106+
def "test response body instrumentation with content-length header"() {
107+
setup:
108+
def request = request(HttpServerTest.ServerEndpoint.CREATED, "GET", null).build()
109+
110+
when:
111+
def response = client.newCall(request).execute()
112+
113+
then:
114+
response.code() == 201
115+
response.body().string() == "content-length-response"
116+
response.header("Content-Length") == "23"
117+
response.header("X-Stream-Wrapped") == "true"
118+
}
119+
120+
def "test response body instrumentation with character encoding"() {
121+
setup:
122+
def request = request(HttpServerTest.ServerEndpoint.REDIRECT, "GET", null).build()
123+
124+
when:
125+
def response = client.newCall(request).execute()
126+
127+
then:
128+
response.code() == 302
129+
response.header("X-Writer-Wrapped") == "true"
130+
response.header("Content-Type").contains("UTF-8")
131+
}
132+
133+
def "test response body instrumentation does not wrap when callbacks are not available"() {
134+
setup:
135+
// Temporarily reset callbacks to test the negative case
136+
ig.reset()
137+
def request = request(HttpServerTest.ServerEndpoint.SUCCESS, "GET", null).build()
138+
139+
when:
140+
def response = client.newCall(request).execute()
141+
142+
then:
143+
response.code() == 200
144+
response.body().string() == "output-stream-response"
145+
146+
and:
147+
// Verify that the instrumentation was NOT applied
148+
response.header("X-Stream-Wrapped") == null
149+
150+
cleanup:
151+
// Restore callbacks for other tests
152+
setupAppSecCallbacks()
153+
}
154+
155+
private void setupAppSecCallbacks() {
156+
Events<Object> events = Events.get()
157+
ig.registerCallback(events.responseBodyStart(), { RequestContext ctx, StoredBodySupplier supplier ->
158+
return null
159+
} as BiFunction<RequestContext, StoredBodySupplier, Void>)
160+
161+
ig.registerCallback(events.responseBodyDone(), { RequestContext ctx, StoredBodySupplier supplier ->
162+
return datadog.trace.api.gateway.Flow.ResultFlow.empty()
163+
} as BiFunction<RequestContext, StoredBodySupplier, datadog.trace.api.gateway.Flow<Void>>)
164+
}
165+
166+
@WebServlet
167+
static class ResponseBodyTestServlet extends AbstractHttpServlet {
168+
@Override
169+
protected void service(HttpServletRequest req, HttpServletResponse resp) {
170+
String path = req.getPathInfo() ?: req.getServletPath()
171+
172+
resp.addHeader(HttpServerTest.IG_RESPONSE_HEADER, HttpServerTest.IG_RESPONSE_HEADER_VALUE)
173+
174+
switch (path) {
175+
case "/success":
176+
testOutputStream(resp, "output-stream-response")
177+
break
178+
case "/forwarded":
179+
testWriter(resp, "writer-response")
180+
break
181+
case "/query":
182+
testBothOutputStreamAndWriter(resp, "both-response")
183+
break
184+
case "/created":
185+
testOutputStreamWithContentLength(resp, "content-length-response")
186+
break
187+
case "/redirect":
188+
testWriterWithEncoding(resp)
189+
break
190+
default:
191+
resp.setStatus(404)
192+
resp.getWriter().print("Not Found")
193+
}
194+
}
195+
196+
private void testOutputStream(HttpServletResponse resp, String content) {
197+
resp.setStatus(200)
198+
resp.setContentType("text/plain")
199+
200+
ServletOutputStream os = resp.getOutputStream()
201+
202+
// Check if the output stream was wrapped
203+
if (os instanceof Servlet31OutputStreamWrapper) {
204+
resp.setHeader("X-Stream-Wrapped", "true")
205+
}
206+
207+
os.write(content.getBytes())
208+
}
209+
210+
private void testWriter(HttpServletResponse resp, String content) {
211+
resp.setStatus(200)
212+
resp.setContentType("text/plain")
213+
214+
PrintWriter writer = resp.getWriter()
215+
216+
// Check if the writer was wrapped
217+
if (writer instanceof BufferedWriterWrapper) {
218+
resp.setHeader("X-Writer-Wrapped", "true")
219+
}
220+
221+
writer.print(content)
222+
}
223+
224+
private void testBothOutputStreamAndWriter(HttpServletResponse resp, String content) {
225+
resp.setStatus(200)
226+
resp.setContentType("text/plain")
227+
228+
try {
229+
// Try to get output stream first
230+
ServletOutputStream os = resp.getOutputStream()
231+
if (os instanceof Servlet31OutputStreamWrapper) {
232+
resp.setHeader("X-Stream-Wrapped", "true")
233+
}
234+
os.write(content.getBytes())
235+
} catch (IllegalStateException e) {
236+
// If output stream fails, try writer
237+
PrintWriter writer = resp.getWriter()
238+
if (writer instanceof BufferedWriterWrapper) {
239+
resp.setHeader("X-Writer-Wrapped", "true")
240+
}
241+
writer.print(content)
242+
}
243+
}
244+
245+
private void testOutputStreamWithContentLength(HttpServletResponse resp, String content) {
246+
resp.setStatus(201)
247+
resp.setContentType("text/plain")
248+
resp.setContentLength(content.length())
249+
250+
ServletOutputStream os = resp.getOutputStream()
251+
252+
if (os instanceof Servlet31OutputStreamWrapper) {
253+
resp.setHeader("X-Stream-Wrapped", "true")
254+
}
255+
256+
os.write(content.getBytes())
257+
}
258+
259+
private void testWriterWithEncoding(HttpServletResponse resp) {
260+
resp.setStatus(302)
261+
resp.setContentType("text/plain; charset=UTF-8")
262+
resp.setCharacterEncoding("UTF-8")
263+
resp.setHeader("Location", "/redirected")
264+
265+
PrintWriter writer = resp.getWriter()
266+
267+
if (writer instanceof BufferedWriterWrapper) {
268+
resp.setHeader("X-Writer-Wrapped", "true")
269+
}
270+
271+
writer.print("Redirecting...")
272+
}
273+
}
274+
}

0 commit comments

Comments
 (0)