This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 6bf01d4a6b1 NIFI-16332 Fixed duplicate Content-Type header on 
replicated upload requests (#11665)
6bf01d4a6b1 is described below

commit 6bf01d4a6b177b54c13f7caf627d0828833ef7e0
Author: Sönke Liebau <[email protected]>
AuthorDate: Thu Sep 10 18:01:39 2026 +0200

    NIFI-16332 Fixed duplicate Content-Type header on replicated upload 
requests (#11665)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../StandardUploadRequestReplicator.java           | 14 +++-
 ...TestStandardUploadRequestReplicatorHeaders.java | 88 ++++++++++++++++++++++
 2 files changed, 100 insertions(+), 2 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/StandardUploadRequestReplicator.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/StandardUploadRequestReplicator.java
index 2847435bcde..deaa2a3cb80 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/StandardUploadRequestReplicator.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/StandardUploadRequestReplicator.java
@@ -176,7 +176,8 @@ public class StandardUploadRequestReplicator implements 
UploadRequestReplicator
      *   <li>Start with any forwarded inbound servlet headers.</li>
      *   <li>Strip all {@link RequestReplicationHeader} names (prevent 
spoofing).</li>
      *   <li>Strip hop-by-hop / transport-framing headers.</li>
-     *   <li>Apply explicit builder headers (filename, content-type, seed) so 
upload metadata wins.</li>
+     *   <li>Apply explicit builder headers (filename, content-type, seed) 
case-insensitively so upload
+     *       metadata wins without leaving a differently-cased inbound 
duplicate.</li>
      *   <li>Apply user proxy headers and strip credentials (Authorization, 
auth cookies, Host).</li>
      *   <li>Force-set {@code request-replicated} and {@code 
execution-continue}.</li>
      * </ol>
@@ -187,7 +188,16 @@ public class StandardUploadRequestReplicator implements 
UploadRequestReplicator
         ReplicationHeaderUtils.stripRequestReplicationHeaders(headers);
         ReplicationHeaderUtils.stripHopByHopHeaders(headers);
 
-        headers.putAll(uploadRequest.getHeaders());
+        // Apply explicit builder headers case-insensitively. Inbound header 
names can arrive in a variety of lower-
+        // and upper-case variations, which we only have limited control over.
+        // So a plain putAll into the case-sensitive header map would risk 
keeping duplicated headers that only differ
+        // in the capitalization.
+        // In the case of "Content-Type" this can create actual issues, 
because requests with a duplicate default header
+        // have to be considered malformed and are rejected with http  400.
+        for (final Map.Entry<String, String> builderHeader : 
uploadRequest.getHeaders().entrySet()) {
+            
headers.keySet().removeIf(builderHeader.getKey()::equalsIgnoreCase);
+            headers.put(builderHeader.getKey(), builderHeader.getValue());
+        }
 
         ReplicationHeaderUtils.applyUserProxyAndStripCredentials(headers, 
uploadRequest.getUser());
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
index 64125696c88..871dd1015c5 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
@@ -31,6 +31,7 @@ import java.io.IOException;
 import java.net.URI;
 import java.nio.file.Path;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 
@@ -181,6 +182,93 @@ class TestStandardUploadRequestReplicatorHeaders {
         assertEquals("explicit-name.txt", result.get(FILENAME_HEADER));
     }
 
+    @Test
+    void testForwardedContentTypeCaseVariantCollapsesToSingleHeader() {
+        // Regression for the NAR upload 400 in cluster mode: HTTP/2 (and 
Envoy) lowercase header names,
+        // so the forwarded inbound header arrives as "content-type" while the 
builder adds "Content-Type".
+        // Without case-insensitive merging both survive in the outbound 
request, and a duplicate singleton
+        // field such as Content-Type is rejected by the receiving node with 
400.
+        final Map<String, String> forwarded = new HashMap<>();
+        forwarded.put("content-type", CONTENT_TYPE_VALUE);
+
+        final UploadRequest<String> request = buildUploadRequest(forwarded);
+        final Map<String, String> result = 
replicator.buildOutboundHeaders(request);
+
+        final List<String> contentTypeKeys = result.keySet().stream()
+                .filter(CONTENT_TYPE_HEADER::equalsIgnoreCase)
+                .toList();
+        assertEquals(List.of(CONTENT_TYPE_HEADER), contentTypeKeys,
+                "Expected a single Content-Type header, but got: " + 
result.keySet());
+        assertEquals(CONTENT_TYPE_VALUE, result.get(CONTENT_TYPE_HEADER));
+    }
+
+    @Test
+    void 
testMultipleForwardedContentTypeCaseVariantsAllCollapseToSingleHeader() {
+        // Defensive: a single servlet request cannot deliver multiple 
case-variants of one header
+        // (the container collapses them), but buildOutboundHeaders must still 
remove every inbound
+        // case-variant, not just the first, so a differently-cased builder 
header cannot leave strays.
+        final Map<String, String> forwarded = new HashMap<>();
+        forwarded.put("content-type", CONTENT_TYPE_VALUE);
+        forwarded.put("Content-TYPE", CONTENT_TYPE_VALUE);
+        forwarded.put("CONTENT-type", CONTENT_TYPE_VALUE);
+
+        final UploadRequest<String> request = buildUploadRequest(forwarded);
+        final Map<String, String> result = 
replicator.buildOutboundHeaders(request);
+
+        final List<String> contentTypeKeys = result.keySet().stream()
+                .filter(CONTENT_TYPE_HEADER::equalsIgnoreCase)
+                .toList();
+        assertEquals(List.of(CONTENT_TYPE_HEADER), contentTypeKeys,
+                "Expected a single Content-Type header, but got: " + 
result.keySet());
+        assertEquals(CONTENT_TYPE_VALUE, result.get(CONTENT_TYPE_HEADER));
+    }
+
+    @Test
+    void testUnrelatedForwardedHeadersUntouchedWhileCollisionCollapses() {
+        // The case-insensitive removal must be scoped to the builder's own 
header names: a Content-Type
+        // collision collapses to one, while unrelated forwarded headers pass 
through verbatim - exact key
+        // case preserved and value unchanged.
+        final Map<String, String> forwarded = new HashMap<>();
+        forwarded.put("content-type", CONTENT_TYPE_VALUE);   // collides with 
builder "Content-Type"
+        forwarded.put("Content-TYPE", CONTENT_TYPE_VALUE);   // a second 
variant
+        forwarded.put(CUSTOM_HEADER, CUSTOM_HEADER_VALUE);   // unrelated, 
mixed case
+        forwarded.put(CUSTOM_TOKEN_HEADER, CUSTOM_TOKEN_VALUE);
+
+        final UploadRequest<String> request = buildUploadRequest(forwarded);
+        final Map<String, String> result = 
replicator.buildOutboundHeaders(request);
+
+        // Content-Type collapses to a single canonical entry with the 
builder's value.
+        final List<String> contentTypeKeys = result.keySet().stream()
+                .filter(CONTENT_TYPE_HEADER::equalsIgnoreCase)
+                .toList();
+        assertEquals(List.of(CONTENT_TYPE_HEADER), contentTypeKeys,
+                "Expected a single Content-Type header, but got: " + 
result.keySet());
+        assertEquals(CONTENT_TYPE_VALUE, result.get(CONTENT_TYPE_HEADER));
+
+        // Unrelated forwarded headers are untouched: exact key case preserved 
and value unchanged.
+        assertTrue(result.containsKey(CUSTOM_HEADER));
+        assertEquals(CUSTOM_HEADER_VALUE, result.get(CUSTOM_HEADER));
+        assertTrue(result.containsKey(CUSTOM_TOKEN_HEADER));
+        assertEquals(CUSTOM_TOKEN_VALUE, result.get(CUSTOM_TOKEN_HEADER));
+    }
+
+    @Test
+    void testLowercaseForwardedFilenameCollapsesToSingleCanonicalHeader() {
+        // Same case-collision guard for the Filename header (lowercased 
inbound + canonical builder header).
+        final Map<String, String> forwarded = new HashMap<>();
+        forwarded.put("filename", "forwarded-name.txt");
+
+        final UploadRequest<String> request = buildUploadRequest(forwarded);
+        final Map<String, String> result = 
replicator.buildOutboundHeaders(request);
+
+        final List<String> filenameKeys = result.keySet().stream()
+                .filter(FILENAME_HEADER::equalsIgnoreCase)
+                .toList();
+        assertEquals(List.of(FILENAME_HEADER), filenameKeys,
+                "Expected a single, canonically-cased Filename header, but 
got: " + result.keySet());
+        assertEquals(TEST_FILENAME, result.get(FILENAME_HEADER));
+    }
+
     @Test
     void testProxiedEntitiesSetFromUser() {
         final Map<String, String> forwarded = new HashMap<>();

Reply via email to