66import datadog .trace .api .gateway .Flow ;
77import datadog .trace .api .gateway .RequestContext ;
88import datadog .trace .api .http .MultipartContentDecoder ;
9- import java .io .IOException ;
109import java .io .InputStream ;
1110import java .lang .reflect .Method ;
1211import java .util .ArrayList ;
12+ import java .util .HashMap ;
1313import java .util .List ;
1414import java .util .Map ;
1515import java .util .Map .Entry ;
1616import org .jboss .resteasy .plugins .providers .multipart .InputPart ;
1717import org .jboss .resteasy .plugins .providers .multipart .MultipartFormDataInput ;
18+ import org .slf4j .Logger ;
19+ import org .slf4j .LoggerFactory ;
1820
1921public final class MultipartHelper {
2022
23+ private static final Logger log = LoggerFactory .getLogger (MultipartHelper .class );
24+
2125 public static final int MAX_CONTENT_BYTES = Config .get ().getAppSecMaxFileContentBytes ();
2226 public static final int MAX_FILES_TO_INSPECT = Config .get ().getAppSecMaxFileContentCount ();
2327
@@ -35,6 +39,78 @@ private MultipartHelper() {}
3539 GET_HEADERS = m ;
3640 }
3741
42+ /**
43+ * Builds the {@code server.request.body} map out of the multipart parts.
44+ *
45+ * <p>Only text/plain parts are collected, matching the Jersey reference: file parts are reported
46+ * separately via {@link #collectFilenames} / {@link #collectFilesContent} and must not also
47+ * consume the body-map budget. The number of collected values is capped by {@link
48+ * #MAX_FILES_TO_INSPECT}. The cap counts the total accumulated values across all field names, not
49+ * the distinct keys: {@code getFormDataMap()} already groups parts by field name, so a per-key
50+ * cap would be trivially bypassed by repeating the same field name on every part.
51+ */
52+ public static Map <String , List <String >> collectBodyMap (MultipartFormDataInput ret ) {
53+ Map <String , List <String >> bodyMap = new HashMap <>();
54+ int total = 0 ;
55+ for (Map .Entry <String , List <InputPart >> e : ret .getFormDataMap ().entrySet ()) {
56+ for (InputPart inputPart : e .getValue ()) {
57+ String contentType = contentTypeOf (inputPart );
58+ if (!isTextPlain (contentType )) {
59+ continue ;
60+ }
61+ if (total >= MAX_FILES_TO_INSPECT ) {
62+ return bodyMap ;
63+ }
64+ bodyMap
65+ .computeIfAbsent (e .getKey (), k -> new ArrayList <>())
66+ .add (readContent (inputPart , contentTypeWithDefaultUtf8 (contentType )));
67+ total ++;
68+ }
69+ }
70+ return bodyMap ;
71+ }
72+
73+ // No Content-Type header means RESTEasy defaults the part to text/plain (see InputPart's own
74+ // javadoc), so a null contentType is treated as text/plain here as well.
75+ private static boolean isTextPlain (String contentType ) {
76+ if (contentType == null ) {
77+ return true ;
78+ }
79+ int semi = contentType .indexOf (';' );
80+ String mediaType = (semi >= 0 ? contentType .substring (0 , semi ) : contentType ).trim ();
81+ return mediaType .equalsIgnoreCase ("text/plain" );
82+ }
83+
84+ // Used for the body-map/text-field path only: matches Jersey's own getValue(), which decodes
85+ // undeclared-charset text parts as UTF-8 instead of falling back to the JVM platform charset
86+ // (MultipartContentDecoder's default for the filesContent path, kept as-is for parity with the
87+ // other multipart integrations).
88+ private static String contentTypeWithDefaultUtf8 (String contentType ) {
89+ return MultipartContentDecoder .extractCharset (contentType ) == null
90+ ? (contentType == null ? "charset=UTF-8" : contentType + "; charset=UTF-8" )
91+ : contentType ;
92+ }
93+
94+ private static String contentTypeOf (InputPart inputPart ) {
95+ if (GET_HEADERS == null ) {
96+ return null ;
97+ }
98+ try {
99+ @ SuppressWarnings ("unchecked" )
100+ Map <String , List <String >> headers = (Map <String , List <String >>) GET_HEADERS .invoke (inputPart );
101+ if (headers == null ) {
102+ return null ;
103+ }
104+ List <String > ctHeaders = getHeaderCaseInsensitive (headers , "Content-Type" );
105+ return (ctHeaders != null && !ctHeaders .isEmpty ()) ? ctHeaders .get (0 ) : null ;
106+ } catch (Exception e ) {
107+ // Reflective getHeaders() call failed (unexpected InputPart implementation): fall back to
108+ // resolving no content-type rather than aborting the whole request's body-map collection.
109+ log .debug ("Failed to read multipart part headers via reflection" , e );
110+ return null ;
111+ }
112+ }
113+
38114 public static List <String > collectFilenames (MultipartFormDataInput ret ) {
39115 List <String > filenames = new ArrayList <>();
40116 if (GET_HEADERS == null ) {
@@ -121,7 +197,10 @@ static String readContent(InputPart inputPart, String contentType) {
121197 try (InputStream is = inputPart .getBody (InputStream .class , null )) {
122198 if (is == null ) return "" ;
123199 return MultipartContentDecoder .readInputStream (is , MAX_CONTENT_BYTES , contentType );
124- } catch (IOException ignored ) {
200+ } catch (Exception e ) {
201+ // getBody()/readInputStream() can throw unchecked exceptions too (e.g. a MessageBodyReader
202+ // lookup failure); one bad part must not abort the whole request's body/content collection.
203+ log .debug ("Failed to read multipart part content, returning empty string" , e );
125204 return "" ;
126205 }
127206 }
0 commit comments