From ace3f1abca67df601a7623b6f4012926cea3408e Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 24 Jun 2026 12:25:15 +0200 Subject: [PATCH 1/4] feat(appsec): add server.request.body.files_content support for Akka HTTP - Extend handleMultipartStrictFormData (strictUnmarshaller) and handleStrictFormData (formFieldMultiMap) in UnmarshallerHelpers to accumulate file content via MultipartContentDecoder and dispatch EVENTS.requestFilesContent() callback - Content dispatch is sequential after filenames (fires only if no prior block), consistent with other frameworks (Tomcat, Netty, Jersey) - Uses ByteString.take(MAX_CONTENT_BYTES).toArray() to avoid full allocation; MAX_* constants read from Config - Add testBodyFilesContent() overrides to both akka-http-10.0 and akka-http-10.6 test modules --- .../AkkaHttpServerInstrumentationTest.groovy | 20 +++ .../akkahttp/appsec/UnmarshallerHelpers.java | 122 +++++++++++++----- .../AkkaHttpServerInstrumentationTest.groovy | 20 +++ 3 files changed, 130 insertions(+), 32 deletions(-) diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy index 65ed26909e7..a1aa9510a93 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/baseTest/groovy/AkkaHttpServerInstrumentationTest.groovy @@ -79,6 +79,11 @@ abstract class AkkaHttpServerInstrumentationTest extends HttpServerTest, Flow> filenamesCallback = cbp.getCallback(EVENTS.requestFilesFilenames()); - if (bodyCallback == null && filenamesCallback == null) { + BiFunction, Flow> contentCallback = + cbp.getCallback(EVENTS.requestFilesContent()); + if (bodyCallback == null && filenamesCallback == null && contentCallback == null) { return; } @@ -204,13 +210,19 @@ private static void handleMultipartStrictFormData( st.getStrictParts(); Map> conv = new HashMap<>(); List filenames = filenamesCallback != null ? new ArrayList<>() : null; + List filesContent = contentCallback != null ? new ArrayList<>() : null; for (akka.http.javadsl.model.Multipart.FormData.BodyPart.Strict part : strictParts) { Optional filenameOpt = part.getFilename(); if (filenames != null && filenameOpt.isPresent() && !filenameOpt.get().isEmpty()) { filenames.add(filenameOpt.get()); } - if (bodyCallback == null) { + boolean needsEntity = + bodyCallback != null + || (filesContent != null + && filenameOpt.isPresent() + && filesContent.size() < MAX_FILES_TO_INSPECT); + if (!needsEntity) { continue; } @@ -221,19 +233,30 @@ private static void handleMultipartStrictFormData( HttpEntity.Strict sentity = (HttpEntity.Strict) entity; - String name = part.getName(); - List curStrings = conv.get(name); - if (curStrings == null) { - curStrings = new ArrayList<>(); - conv.put(name, curStrings); + if (bodyCallback != null) { + String name = part.getName(); + List curStrings = conv.get(name); + if (curStrings == null) { + curStrings = new ArrayList<>(); + conv.put(name, curStrings); + } + + String s = + sentity + .getData() + .decodeString( + Unmarshaller$.MODULE$.bestUnmarshallingCharsetFor(sentity).nioCharset()); + curStrings.add(s); } - String s = - sentity - .getData() - .decodeString( - Unmarshaller$.MODULE$.bestUnmarshallingCharsetFor(sentity).nioCharset()); - curStrings.add(s); + if (filesContent != null + && filenameOpt.isPresent() + && filesContent.size() < MAX_FILES_TO_INSPECT) { + byte[] bytes = sentity.getData().take(MAX_CONTENT_BYTES).toArray(); + filesContent.add( + MultipartContentDecoder.decodeBytes( + bytes, bytes.length, entity.getContentType().toString())); + } } BlockingException pendingBlock = null; @@ -260,6 +283,18 @@ private static void handleMultipartStrictFormData( } } + if (pendingBlock == null && filesContent != null && !filesContent.isEmpty()) { + Flow flow = contentCallback.apply(reqCtx, filesContent); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + pendingBlock = + tryBlock( + reqCtx, + (Flow.Action.RequestBlockingAction) action, + "multipart file upload content"); + } + } + if (pendingBlock != null) { throw pendingBlock; } @@ -419,10 +454,13 @@ private static void handleStrictFormData(StrictForm sf) { CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); BiFunction, Flow> filenamesCb = cbp.getCallback(EVENTS.requestFilesFilenames()); + BiFunction, Flow> contentCb = + cbp.getCallback(EVENTS.requestFilesContent()); Iterator> iterator = sf.fields().iterator(); Map> conv = new HashMap<>(); List filenames = filenamesCb != null ? new ArrayList<>() : null; + List filesContent = contentCb != null ? new ArrayList<>() : null; while (iterator.hasNext()) { Tuple2 next = iterator.next(); String fieldName = next._1(); @@ -449,13 +487,19 @@ private static void handleStrictFormData(StrictForm sf) { instanceof akka.http.scaladsl.model.Multipart$FormData$BodyPart$Strict) { akka.http.scaladsl.model.Multipart$FormData$BodyPart$Strict bodyPart = (akka.http.scaladsl.model.Multipart$FormData$BodyPart$Strict) strictFieldValue; - if (filenames != null) { - Optional filenameOpt = bodyPart.getFilename(); - if (filenameOpt.isPresent() && !filenameOpt.get().isEmpty()) { - filenames.add(filenameOpt.get()); - } + Optional filenameOpt = bodyPart.getFilename(); + if (filenames != null && filenameOpt.isPresent() && !filenameOpt.get().isEmpty()) { + filenames.add(filenameOpt.get()); } HttpEntity.Strict sentity = bodyPart.entity(); + if (filesContent != null + && filenameOpt.isPresent() + && filesContent.size() < MAX_FILES_TO_INSPECT) { + byte[] bytes = sentity.getData().take(MAX_CONTENT_BYTES).toArray(); + filesContent.add( + MultipartContentDecoder.decodeBytes( + bytes, bytes.length, sentity.contentType().toString())); + } String s = sentity .getData() @@ -472,24 +516,38 @@ private static void handleStrictFormData(StrictForm sf) { pendingBlock = e; } - if (filenamesCb != null && filenames != null && !filenames.isEmpty()) { - AgentSpan span = activeSpan(); - RequestContext reqCtx; - if (span != null - && (reqCtx = span.getRequestContext()) != null - && reqCtx.getData(RequestContextSlot.APPSEC) != null) { - Flow flow = filenamesCb.apply(reqCtx, filenames); - if (pendingBlock == null) { - Flow.Action action = flow.getAction(); - if (action instanceof Flow.Action.RequestBlockingAction) { - pendingBlock = - tryBlock( - reqCtx, (Flow.Action.RequestBlockingAction) action, "multipart file upload"); - } + AgentSpan span = activeSpan(); + RequestContext reqCtx = null; + if (span != null) { + RequestContext ctx = span.getRequestContext(); + if (ctx != null && ctx.getData(RequestContextSlot.APPSEC) != null) { + reqCtx = ctx; + } + } + + if (reqCtx != null && filenamesCb != null && filenames != null && !filenames.isEmpty()) { + Flow flow = filenamesCb.apply(reqCtx, filenames); + if (pendingBlock == null) { + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + pendingBlock = + tryBlock(reqCtx, (Flow.Action.RequestBlockingAction) action, "multipart file upload"); } } } + if (pendingBlock == null && reqCtx != null && contentCb != null && !filesContent.isEmpty()) { + Flow flow = contentCb.apply(reqCtx, filesContent); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + pendingBlock = + tryBlock( + reqCtx, + (Flow.Action.RequestBlockingAction) action, + "multipart file upload content"); + } + } + if (pendingBlock != null) { throw pendingBlock; } diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy index 1fe0b3a3be0..9a4815ed382 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy @@ -80,6 +80,11 @@ abstract class AkkaHttpServerInstrumentationTest extends HttpServerTest Date: Wed, 24 Jun 2026 12:42:56 +0200 Subject: [PATCH 2/4] fix(appsec): skip body decoding in handleStrictFormData when body callback absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When only files_content is subscribed (not requestBodyProcessed), the previous code still called getData().decodeString() on every file body part to populate conv — decoding the full file into a String regardless of MAX_CONTENT_BYTES. Gate decodeString and handleArbitraryPostData on bodyCb != null to avoid unnecessary full-file allocation. --- .../akkahttp/appsec/UnmarshallerHelpers.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/UnmarshallerHelpers.java b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/UnmarshallerHelpers.java index fe5fa4bd587..78c791612a5 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/UnmarshallerHelpers.java +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/UnmarshallerHelpers.java @@ -452,10 +452,15 @@ public static Unmarshaller transformStrictFormUnmarshall private static void handleStrictFormData(StrictForm sf) { CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> bodyCb = + cbp.getCallback(EVENTS.requestBodyProcessed()); BiFunction, Flow> filenamesCb = cbp.getCallback(EVENTS.requestFilesFilenames()); BiFunction, Flow> contentCb = cbp.getCallback(EVENTS.requestFilesContent()); + if (bodyCb == null && filenamesCb == null && contentCb == null) { + return; + } Iterator> iterator = sf.fields().iterator(); Map> conv = new HashMap<>(); @@ -500,20 +505,24 @@ private static void handleStrictFormData(StrictForm sf) { MultipartContentDecoder.decodeBytes( bytes, bytes.length, sentity.contentType().toString())); } - String s = - sentity - .getData() - .decodeString( - Unmarshaller$.MODULE$.bestUnmarshallingCharsetFor(sentity).nioCharset()); - strings.add(s); + if (bodyCb != null) { + String s = + sentity + .getData() + .decodeString( + Unmarshaller$.MODULE$.bestUnmarshallingCharsetFor(sentity).nioCharset()); + strings.add(s); + } } } BlockingException pendingBlock = null; - try { - handleArbitraryPostData(conv, "HttpEntity -> StrictForm unmarshaller"); - } catch (BlockingException e) { - pendingBlock = e; + if (bodyCb != null) { + try { + handleArbitraryPostData(conv, "HttpEntity -> StrictForm unmarshaller"); + } catch (BlockingException e) { + pendingBlock = e; + } } AgentSpan span = activeSpan(); From 76218cdc28e822d0bb38fce825bf901cde803fe8 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 24 Jun 2026 13:45:29 +0200 Subject: [PATCH 3/4] fix: disable testBodyFilesContent in AkkaHttp102ServerInstrumentationBindSyncTest Sync server binding does not support body processing; override returns false to match the pattern already used for testBodyMultipart, testBodyFilenames, etc. --- .../groovy/AkkaHttp102ServerInstrumentationTests.groovy | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/latestDepTest/groovy/AkkaHttp102ServerInstrumentationTests.groovy b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/latestDepTest/groovy/AkkaHttp102ServerInstrumentationTests.groovy index 6e517914b17..711bf874fa5 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/latestDepTest/groovy/AkkaHttp102ServerInstrumentationTests.groovy +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.0/src/latestDepTest/groovy/AkkaHttp102ServerInstrumentationTests.groovy @@ -41,6 +41,11 @@ class AkkaHttp102ServerInstrumentationBindSyncTest extends AkkaHttpServerInstrum false } + @Override + boolean testBodyFilesContent() { + false + } + @Override boolean testBodyJson() { false From 06ab8fea5dd34063c2ce9db3e68fa16536e18ae7 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 24 Jun 2026 14:06:34 +0200 Subject: [PATCH 4/4] fix: disable testBodyFilesContent in AkkaHttp102ServerInstrumentationBindSyncTest (10.6) Same fix as 10.0: sync binding does not process body, override returns false to match testBodyMultipart, testBodyFilenames, etc. --- .../test/groovy/AkkaHttp102ServerInstrumentationTests.groovy | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttp102ServerInstrumentationTests.groovy b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttp102ServerInstrumentationTests.groovy index 6e517914b17..711bf874fa5 100644 --- a/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttp102ServerInstrumentationTests.groovy +++ b/dd-java-agent/instrumentation/akka/akka-http/akka-http-10.6/src/test/groovy/AkkaHttp102ServerInstrumentationTests.groovy @@ -41,6 +41,11 @@ class AkkaHttp102ServerInstrumentationBindSyncTest extends AkkaHttpServerInstrum false } + @Override + boolean testBodyFilesContent() { + false + } + @Override boolean testBodyJson() { false