Skip to content

Commit 644be01

Browse files
authored
fix: Extract filename and content type from headers in StreamReceiverHandler for XHR uploads (#22690)
StreamReceiverHandler.doHandleXhrFilePost() was using hardcoded "unknown" values for both filename and content type, even though the headers were available in the request. This fix: - Extracts filename from X-Filename header (with proper decoding) - Extracts content type from Content-Type header - Creates reusable helper methods in TransferUtil to avoid duplication with the existing XHR upload handling code (from commit 1c777e1)
1 parent 86b0dbb commit 644be01

File tree

3 files changed

+142
-19
lines changed

3 files changed

+142
-19
lines changed

flow-server/src/main/java/com/vaadin/flow/server/communication/StreamReceiverHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,10 @@ protected void doHandleXhrFilePost(VaadinSession session,
295295
StreamReceiver streamReceiver, StateNode owner, long contentLength)
296296
throws IOException {
297297

298-
// These are unknown in filexhr ATM, maybe add to Accept header that
299-
// is accessible in portlets
300-
final String filename = "unknown";
301-
final String mimeType = filename;
298+
String filename = TransferUtil.extractFilenameFromXhrRequest(request);
299+
String mimeType = TransferUtil
300+
.extractContentTypeFromXhrRequest(request);
301+
302302
final InputStream stream = request.getInputStream();
303303

304304
boolean success = false;

flow-server/src/main/java/com/vaadin/flow/server/communication/TransferUtil.java

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,8 @@ public static void handleUpload(UploadHandler handler,
172172
handler.responseHandled(false, response);
173173
}
174174
} else {
175-
// Extract filename from X-Filename header
176-
// The filename is encoded using JavaScript's encodeURIComponent
177-
String fileName = request.getHeader("X-Filename");
178-
179-
if (fileName == null || fileName.isEmpty()) {
180-
fileName = "unknown";
181-
} else {
182-
// Decode the percent-encoded filename
183-
fileName = UrlUtil.decodeURIComponent(fileName);
184-
}
185-
186-
String contentType = request.getHeader("Content-Type");
187-
if (contentType == null || contentType.isEmpty()) {
188-
contentType = "unknown";
189-
}
175+
String fileName = extractFilenameFromXhrRequest(request);
176+
String contentType = extractContentTypeFromXhrRequest(request);
190177

191178
UploadEvent event = new UploadEvent(request, response, session,
192179
fileName, request.getContentLengthLong(), contentType,
@@ -243,6 +230,49 @@ private static boolean isMultipartContent(VaadinRequest request) {
243230
&& contentType.toLowerCase().startsWith("multipart/");
244231
}
245232

233+
/**
234+
* Extracts the filename from an XHR upload request.
235+
* <p>
236+
* The filename is extracted from the X-Filename header, which is set by
237+
* vaadin-upload. The filename is encoded using JavaScript's
238+
* encodeURIComponent and decoded on the server using
239+
* {@link UrlUtil#decodeURIComponent(String)} (RFC 3986).
240+
*
241+
* @param request
242+
* the request to extract the filename from
243+
* @return the decoded filename, or "unknown" if not present
244+
*/
245+
public static String extractFilenameFromXhrRequest(VaadinRequest request) {
246+
String fileName = request.getHeader("X-Filename");
247+
248+
if (fileName == null || fileName.isEmpty()) {
249+
return "unknown";
250+
}
251+
252+
// Decode the percent-encoded filename
253+
return UrlUtil.decodeURIComponent(fileName);
254+
}
255+
256+
/**
257+
* Extracts the content type from an XHR upload request.
258+
* <p>
259+
* The content type is extracted from the Content-Type header.
260+
*
261+
* @param request
262+
* the request to extract the content type from
263+
* @return the content type, or "unknown" if not present
264+
*/
265+
public static String extractContentTypeFromXhrRequest(
266+
VaadinRequest request) {
267+
String contentType = request.getHeader("Content-Type");
268+
269+
if (contentType == null || contentType.isEmpty()) {
270+
return "unknown";
271+
}
272+
273+
return contentType;
274+
}
275+
246276
/**
247277
* Validates upload limits for the given parts.
248278
*

flow-server/src/test/java/com/vaadin/flow/server/communication/StreamReceiverHandlerTest.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public class StreamReceiverHandlerTest {
103103

104104
private boolean isGetContentLengthLongCalled;
105105
private String requestCharacterEncoding;
106+
private String xFilenameHeader;
106107

107108
@Before
108109
public void setup() throws Exception {
@@ -178,6 +179,12 @@ public String getHeader(String name) {
178179
if ("content-length".equals(name.toLowerCase())) {
179180
return contentLength;
180181
}
182+
if ("x-filename".equals(name.toLowerCase())) {
183+
return xFilenameHeader;
184+
}
185+
if ("content-type".equals(name.toLowerCase())) {
186+
return contentType;
187+
}
181188
return super.getHeader(name);
182189
}
183190

@@ -468,4 +475,90 @@ public void doHandleMultipartFileUpload_IOExceptionIsThrown_exceptionIsNotRethro
468475
Mockito.verifyNoInteractions(errorHandler);
469476
}
470477

478+
@Test
479+
public void doHandleXhrFilePost_filenameFromHeader_extractedCorrectly()
480+
throws IOException {
481+
xFilenameHeader = "test.txt";
482+
outputStream = new ByteArrayOutputStream();
483+
484+
handler.doHandleXhrFilePost(session, request, response, streamReceiver,
485+
stateNode, 6);
486+
487+
ArgumentCaptor<StreamVariable.StreamingEndEvent> endEventCaptor = ArgumentCaptor
488+
.forClass(StreamVariable.StreamingEndEvent.class);
489+
Mockito.verify(streamVariable)
490+
.streamingFinished(endEventCaptor.capture());
491+
Assert.assertEquals("test.txt",
492+
endEventCaptor.getValue().getFileName());
493+
}
494+
495+
@Test
496+
public void doHandleXhrFilePost_encodedFilename_decodedCorrectly()
497+
throws IOException {
498+
// encodeURIComponent("my file åäö.txt") in JavaScript
499+
xFilenameHeader = "my%20file%20%C3%A5%C3%A4%C3%B6.txt";
500+
outputStream = new ByteArrayOutputStream();
501+
502+
handler.doHandleXhrFilePost(session, request, response, streamReceiver,
503+
stateNode, 6);
504+
505+
ArgumentCaptor<StreamVariable.StreamingEndEvent> endEventCaptor = ArgumentCaptor
506+
.forClass(StreamVariable.StreamingEndEvent.class);
507+
Mockito.verify(streamVariable)
508+
.streamingFinished(endEventCaptor.capture());
509+
Assert.assertEquals("my file åäö.txt",
510+
endEventCaptor.getValue().getFileName());
511+
}
512+
513+
@Test
514+
public void doHandleXhrFilePost_contentTypeFromHeader_extractedCorrectly()
515+
throws IOException {
516+
xFilenameHeader = "test.txt";
517+
contentType = "text/plain";
518+
outputStream = new ByteArrayOutputStream();
519+
520+
handler.doHandleXhrFilePost(session, request, response, streamReceiver,
521+
stateNode, 6);
522+
523+
ArgumentCaptor<StreamVariable.StreamingEndEvent> endEventCaptor = ArgumentCaptor
524+
.forClass(StreamVariable.StreamingEndEvent.class);
525+
Mockito.verify(streamVariable)
526+
.streamingFinished(endEventCaptor.capture());
527+
Assert.assertEquals("text/plain",
528+
endEventCaptor.getValue().getMimeType());
529+
}
530+
531+
@Test
532+
public void doHandleXhrFilePost_missingContentTypeHeader_defaultsToUnknown()
533+
throws IOException {
534+
xFilenameHeader = "test.txt";
535+
contentType = null;
536+
outputStream = new ByteArrayOutputStream();
537+
538+
handler.doHandleXhrFilePost(session, request, response, streamReceiver,
539+
stateNode, 6);
540+
541+
ArgumentCaptor<StreamVariable.StreamingEndEvent> endEventCaptor = ArgumentCaptor
542+
.forClass(StreamVariable.StreamingEndEvent.class);
543+
Mockito.verify(streamVariable)
544+
.streamingFinished(endEventCaptor.capture());
545+
Assert.assertEquals("unknown", endEventCaptor.getValue().getMimeType());
546+
}
547+
548+
@Test
549+
public void doHandleXhrFilePost_missingFilenameHeader_defaultsToUnknown()
550+
throws IOException {
551+
xFilenameHeader = null;
552+
outputStream = new ByteArrayOutputStream();
553+
554+
handler.doHandleXhrFilePost(session, request, response, streamReceiver,
555+
stateNode, 6);
556+
557+
ArgumentCaptor<StreamVariable.StreamingEndEvent> endEventCaptor = ArgumentCaptor
558+
.forClass(StreamVariable.StreamingEndEvent.class);
559+
Mockito.verify(streamVariable)
560+
.streamingFinished(endEventCaptor.capture());
561+
Assert.assertEquals("unknown", endEventCaptor.getValue().getFileName());
562+
}
563+
471564
}

0 commit comments

Comments
 (0)