Skip to content

Commit d5f7e5b

Browse files
authored
Fixes #40: Clear Content-Length header on error responses (#120)
1 parent 6489e43 commit d5f7e5b

6 files changed

Lines changed: 70 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ All notable changes to this project will be documented in this file.
2121
- **Default Disk-Based Locking**: `TusFileUploadService.withStoragePath(String)` now defaults to `LeaseFileLockingService` instead of `DiskLockingService` for out-of-the-box Kubernetes, container, and shared network storage compatibility. See `docs/DISK_BASED_LOCKING.md` for legacy opt-out instructions.
2222
- **Calibrated Retry Budget**: Extended `TusFileUploadService` lock acquisition retry budget to 8.0 seconds (40 retries x 200ms) to ensure reliable contention resolution over network storage.
2323

24+
### Fixed
25+
- **Clear Content-Length on Error Responses**: Cleared `Content-Length` response header prior to invoking `HttpServletResponse.sendError(...)` during exception handling, resolving buffer conflicts and exceptions in Undertow and other servlet containers ([#40](https://github.com/tomdesair/tus-java-server/issues/40)).
26+
2427
### Breaking
2528
- **Downloads**: In order to support both the Tus protocol and RUFH protocol, the unofficial download extension will not return a HTTP status code `204` for uploads that are still in progress and will not contain the response header `Tus-Resumable`. Removed the `UploadInProgressException` class.
2629

src/main/java/me/desair/tus/server/TusFileUploadService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ public void process(
496496

497497
} catch (TusException e) {
498498
log.error("Unable to lock upload for request URI " + request.getRequestURI(), e);
499+
response.setHeader(HttpHeader.CONTENT_LENGTH, null);
499500
response.sendError(e.getStatus(), e.getMessage());
500501
}
501502
}
@@ -744,6 +745,7 @@ protected void processTusException(
744745
if (problemDetails != null) {
745746
problemDetails.writeTo(response);
746747
} else {
748+
response.setHeader(HttpHeader.CONTENT_LENGTH, null);
747749
response.sendError(status, message);
748750
}
749751
}

src/main/java/me/desair/tus/server/util/TusServletResponse.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,13 @@ private void recordHeader(String name, String value) {
8282
}
8383

8484
private void overwriteHeader(String name, String value) {
85-
List<String> values = new LinkedList<>();
86-
values.add(value);
87-
headers.put(name, values);
85+
if (value == null) {
86+
headers.remove(name);
87+
} else {
88+
List<String> values = new LinkedList<>();
89+
values.add(value);
90+
headers.put(name, values);
91+
}
8892
}
8993

9094
private String sanitizeHeaderValue(String value) {

src/test/java/me/desair/tus/server/AbstractITTusFileUploadService.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public void testDisableFeature() throws Exception {
121121
servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
122122

123123
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
124-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
124+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
125125
assertResponseStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
126126

127127
reset();
@@ -131,7 +131,7 @@ public void testDisableFeature() throws Exception {
131131

132132
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
133133
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
134-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
134+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
135135
assertResponseStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
136136
}
137137

@@ -258,7 +258,7 @@ public void testProcessCompleteUpload() throws Exception {
258258
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
259259
assertResponseStatus(HttpServletResponse.SC_NOT_FOUND);
260260
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
261-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
261+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
262262
}
263263

264264
@Test
@@ -383,7 +383,7 @@ public void testTerminateViaHttpRequest() throws Exception {
383383
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
384384
assertResponseStatus(HttpServletResponse.SC_NOT_FOUND);
385385
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
386-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
386+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
387387
}
388388

389389
@Test
@@ -766,7 +766,7 @@ public void testProcessUploadInvalidChecksumSecondPart() throws Exception {
766766
// We expect the server to return a checksum mismatch error
767767
assertResponseStatus(460);
768768
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
769-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
769+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
770770

771771
// Check that upload info is still from the first patch
772772
UploadInfo info = tusFileUploadService.getUploadInfo(location, OWNER_KEY);
@@ -814,7 +814,7 @@ public void testProcessUploadInvalidChecksumSecondPart() throws Exception {
814814
tusFileUploadService.process(servletRequest, servletResponse);
815815
assertResponseStatus(HttpServletResponse.SC_NOT_FOUND);
816816
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
817-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
817+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
818818
}
819819

820820
@Test
@@ -879,7 +879,7 @@ public void testCleanupExpiredUpload() throws Exception {
879879

880880
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
881881
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
882-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
882+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
883883
assertResponseStatus(HttpServletResponse.SC_NOT_FOUND);
884884
}
885885

@@ -1490,7 +1490,7 @@ public void testHeadOnNonExistingUpload() throws Exception {
14901490
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
14911491
assertResponseStatus(HttpServletResponse.SC_NOT_FOUND);
14921492
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
1493-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
1493+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
14941494
}
14951495

14961496
@Test
@@ -1504,7 +1504,7 @@ public void testInvalidTusResumable() throws Exception {
15041504
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
15051505
assertResponseStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
15061506
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
1507-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
1507+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
15081508
}
15091509

15101510
@Test
@@ -1523,7 +1523,7 @@ public void testMaxUploadLengthExceeded() throws Exception {
15231523
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
15241524
assertResponseStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
15251525
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
1526-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
1526+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
15271527
}
15281528

15291529
@Test
@@ -1543,7 +1543,7 @@ public void testInvalidMethods() throws Exception {
15431543
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
15441544
assertResponseStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
15451545
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
1546-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
1546+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
15471547

15481548
reset();
15491549
servletRequest.setMethod("TRACE");
@@ -1553,7 +1553,7 @@ public void testInvalidMethods() throws Exception {
15531553
tusFileUploadService.process(servletRequest, servletResponse, OWNER_KEY);
15541554
assertResponseStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
15551555
assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
1556-
assertResponseHeader(HttpHeader.CONTENT_LENGTH, "0");
1556+
assertResponseHeaderNull(HttpHeader.CONTENT_LENGTH);
15571557
}
15581558

15591559
@Test

src/test/java/me/desair/tus/server/TusFileUploadServiceTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package me.desair.tus.server;
22

33
import static org.hamcrest.CoreMatchers.is;
4+
import static org.hamcrest.CoreMatchers.nullValue;
45
import static org.hamcrest.MatcherAssert.assertThat;
56
import static org.junit.Assert.assertEquals;
67
import static org.junit.Assert.assertNotNull;
@@ -168,9 +169,44 @@ public void testProcessLockFailure() throws Exception {
168169

169170
service.process(mockReq, mockResp, "owner");
170171

172+
verify(mockResp, times(1)).setHeader(HttpHeader.CONTENT_LENGTH, null);
171173
verify(mockResp, times(1)).sendError(423, "Locked");
172174
}
173175

176+
@Test
177+
public void testProcessTusExceptionClearsContentLengthHeader() throws Exception {
178+
UploadLockingService mockLockingService = mock(UploadLockingService.class);
179+
UploadLock mockLock = mock(UploadLock.class);
180+
when(mockLockingService.lockUploadByUri(anyString())).thenReturn(mockLock);
181+
182+
UploadStorageService mockStorage = mock(UploadStorageService.class);
183+
when(mockStorage.getUploadInfo(anyString(), anyString())).thenReturn(null);
184+
185+
org.springframework.mock.web.MockHttpServletRequest mockReq =
186+
new org.springframework.mock.web.MockHttpServletRequest();
187+
org.springframework.mock.web.MockHttpServletResponse mockResp =
188+
new org.springframework.mock.web.MockHttpServletResponse();
189+
190+
mockReq.setMethod("PATCH");
191+
mockReq.setRequestURI("/files/test");
192+
mockReq.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
193+
mockReq.addHeader(HttpHeader.CONTENT_TYPE, "application/offset+octet-stream");
194+
mockReq.addHeader(HttpHeader.UPLOAD_OFFSET, "0");
195+
196+
TusFileUploadService service =
197+
new TusFileUploadService()
198+
.withUploadLockingService(mockLockingService)
199+
.withUploadStorageService(mockStorage);
200+
201+
service.process(mockReq, mockResp, "owner");
202+
203+
// Check that Tus-Resumable is preserved while Content-Length is cleared for sendError
204+
assertThat(mockResp.getHeader(HttpHeader.TUS_RESUMABLE), is("1.0.0"));
205+
assertThat(mockResp.getHeader(HttpHeader.CONTENT_LENGTH), is(nullValue()));
206+
assertThat(mockResp.getStatus(), is(404));
207+
verify(mockLock, times(1)).close();
208+
}
209+
174210
@Test
175211
public void testProtocolVersionConfiguration() {
176212
TusFileUploadService service = new TusFileUploadService();

src/test/java/me/desair/tus/server/util/TusServletResponseTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,14 @@ public void addIntHeader() throws Exception {
127127
public void getHeaderNull() throws Exception {
128128
assertThat(tusServletResponse.getHeader("TEST"), is(nullValue()));
129129
}
130+
131+
@Test
132+
public void setHeaderNullClearsHeader() throws Exception {
133+
tusServletResponse.setHeader("TEST", "foo");
134+
assertThat(tusServletResponse.getHeader("TEST"), is("foo"));
135+
136+
tusServletResponse.setHeader("TEST", null);
137+
assertThat(tusServletResponse.getHeader("TEST"), is(nullValue()));
138+
assertThat(servletResponse.getHeader("TEST"), is(nullValue()));
139+
}
130140
}

0 commit comments

Comments
 (0)