• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tomdesair / tus-java-server / 30699386120

01 Aug 2026 12:16PM UTC coverage: 96.519% (+0.2%) from 96.295%
30699386120

push

github

web-flow
feat(rufh): add Python conformity test suite and resolve RUFH draft-12 compliance failures (#110)

* Refactor RUFH 104 interim response handling and add Tomcat Valve documentation

* feat(rufh): add Python conformity test suite and resolve RUFH draft-12 compliance failures

* fix(download): set dual-protocol response headers on GET responses and refine error handling

* fix: Cleanup code

* test: add unit tests for missing branch coverage on RUFH and utility handlers

* refactor: replace generic TusException with typed exceptions across validators and tests

* docs: add typed exception guidelines to AGENTS.md

1392 of 1496 branches covered (93.05%)

Branch coverage included in aggregate %.

192 of 194 new or added lines in 36 files covered. (98.97%)

2933 of 2985 relevant lines covered (98.26%)

6.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.12
/src/main/java/me/desair/tus/server/rufh/validation/RufhAppendValidator.java
1
package me.desair.tus.server.rufh.validation;
2

3
import jakarta.servlet.http.HttpServletRequest;
4
import java.io.IOException;
5
import me.desair.tus.server.HttpHeader;
6
import me.desair.tus.server.HttpMethod;
7
import me.desair.tus.server.RequestValidator;
8
import me.desair.tus.server.exception.InconsistentUploadLengthException;
9
import me.desair.tus.server.exception.InvalidUploadCompleteHeaderException;
10
import me.desair.tus.server.exception.InvalidUploadOffsetHeaderException;
11
import me.desair.tus.server.exception.MaxAppendSizeExceededException;
12
import me.desair.tus.server.exception.MinAppendSizeNotMetException;
13
import me.desair.tus.server.exception.TusException;
14
import me.desair.tus.server.exception.UnsupportedMediaTypeException;
15
import me.desair.tus.server.exception.UploadAlreadyCompletedException;
16
import me.desair.tus.server.exception.UploadLengthExceededException;
17
import me.desair.tus.server.exception.UploadNotFoundException;
18
import me.desair.tus.server.exception.UploadOffsetMismatchException;
19
import me.desair.tus.server.upload.UploadInfo;
20
import me.desair.tus.server.upload.UploadStorageService;
21
import me.desair.tus.server.util.StructuredHeaderUtil;
22
import me.desair.tus.server.util.Utils;
23
import org.apache.commons.lang3.Strings;
24

25
/**
26
 * Request validator checking data append requests via HTTP PATCH.
27
 *
28
 * <p>Validates Content-Type headers, resource existence, completed upload status, payload limits,
29
 * Upload-Offset equality, and Upload-Length compliance.
30
 *
31
 * <p>Reference: Section 4.4.1 (Append Request) & Section 4.4.2 (Append Response) of
32
 * draft-ietf-httpbis-resumable-upload-12:
33
 *
34
 * <ul>
35
 *   <li>"If the Upload-Offset request header field value does not match the current offset... the
36
 *       upload resource MUST reject the request with a 409 (Conflict) status code."
37
 *   <li>"If the length is known, the server MUST prevent the offset from exceeding the upload
38
 *       length by rejecting the request once the offset exceeds the length..."
39
 * </ul>
40
 */
41
public class RufhAppendValidator implements RequestValidator {
6✔
42

43
  @Override
44
  public boolean supports(HttpMethod method) {
45
    return HttpMethod.PATCH.equals(method);
8✔
46
  }
47

48
  @Override
49
  public void validate(
50
      HttpMethod method,
51
      HttpServletRequest request,
52
      UploadStorageService uploadStorageService,
53
      String ownerKey)
54
      throws TusException, IOException {
55

56
    String requestUri = request.getRequestURI();
3✔
57
    boolean isCreationEndpoint = Utils.isCreationEndpoint(request, uploadStorageService);
4✔
58
    UploadInfo uploadInfo = uploadStorageService.getUploadInfo(requestUri, ownerKey);
5✔
59

60
    if (uploadInfo == null || uploadInfo.isExpired()) {
5!
61
      if (!isCreationEndpoint) {
2✔
62
        throw new UploadNotFoundException("Upload resource not found");
5✔
63
      }
64
      return;
1✔
65
    }
66

67
    String uploadCompleteHeader = request.getHeader(HttpHeader.UPLOAD_COMPLETE);
4✔
68
    if (uploadCompleteHeader == null) {
2✔
69
      throw new InvalidUploadCompleteHeaderException(
5✔
70
          "PATCH append request MUST include Upload-Complete header field");
71
    }
72

73
    String contentType = request.getHeader(HttpHeader.CONTENT_TYPE);
4✔
74
    if (!Strings.CS.startsWith(contentType, HttpHeader.CONTENT_TYPE_PARTIAL_UPLOAD)
8✔
75
        && !Strings.CS.startsWith(contentType, "application/offset+octet-stream")) {
2✔
76
      throw new UnsupportedMediaTypeException("Unsupported Content-Type for append request");
5✔
77
    }
78

79
    if (!uploadInfo.isUploadInProgress()) {
3✔
80
      try {
81
        // Section 4.4.2: Deactivate upload resource when append is attempted on a completed upload
82
        // Terminate/deactivate upload resource per §4.4.2 when append is attempted past
83
        // declared length
84
        uploadStorageService.terminateUpload(uploadInfo);
3✔
85
      } catch (Exception e) {
1✔
86
        // Log or ignore cleanup failure
87
      }
1✔
88
      throw new UploadAlreadyCompletedException("Upload resource is already completed");
5✔
89
    }
90

91
    String offsetHeader = request.getHeader(HttpHeader.UPLOAD_OFFSET);
4✔
92
    Long providedOffset = StructuredHeaderUtil.parseInteger(offsetHeader);
3✔
93
    if (providedOffset == null) {
2✔
94
      throw new InvalidUploadOffsetHeaderException("Missing or invalid Upload-Offset header");
5✔
95
    }
96

97
    long currentOffset = uploadInfo.getOffset();
4✔
98
    if (providedOffset != currentOffset) {
5✔
99
      // Section 4.4.2: Offset Mismatch Error Response
100
      // "If the Upload-Offset request header field value does not match the current offset... the
101
      // server MUST reject
102
      // the request with a 409 (Conflict) status code... The response MUST include the correct
103
      // offset in the Upload-Offset header field."
104
      throw new UploadOffsetMismatchException(
7✔
105
          "Upload-Offset " + providedOffset + " does not match server offset " + currentOffset);
106
    }
107

108
    long contentLength = request.getContentLengthLong();
3✔
109

110
    // Section 4.1.4 & Section 4.7: Validate max-append-size first to reject oversized payloads with
111
    // 413 Payload Too Large
112
    Long maxAppendSize = uploadStorageService.getMaxAppendSize();
3✔
113
    if (maxAppendSize != null
3✔
114
        && maxAppendSize > 0
10✔
115
        && contentLength > 0
116
        && contentLength > maxAppendSize) {
3!
NEW
117
      throw new MaxAppendSizeExceededException(
×
118
          "The request payload size ("
119
              + contentLength
120
              + ") exceeds the maximum allowed append size ("
121
              + maxAppendSize
122
              + ")");
123
    }
124

125
    // Section 4.4.2: Prevent offset from exceeding upload length if length is known and invalidate
126
    // resource
127
    // "the server MUST prevent the offset from exceeding the representation's length by rejecting
128
    // the request
129
    // once the offset exceeds the length, marking the upload resource invalid and rejecting any
130
    // further interaction with it."
131
    // When appended bytes cause offset to exceed declared length (or if upload is already
132
    // at declared length), deactivate/terminate the upload resource per §4.4.2 and reject
133
    // with 409 Conflict.
134
    if (uploadInfo.hasLength()) {
3✔
135
      if (currentOffset >= uploadInfo.getLength()
14!
136
          || (contentLength > 0 && currentOffset + contentLength > uploadInfo.getLength())) {
4✔
137
        try {
138
          uploadStorageService.terminateUpload(uploadInfo);
3✔
139
        } catch (Exception e) {
1✔
140
          // Log or ignore cleanup failure
141
        }
1✔
142
        throw new UploadLengthExceededException(
4✔
143
            "Appended content length ("
144
                + contentLength
145
                + ") pushes total offset past declared upload length ("
146
                + uploadInfo.getLength()
4✔
147
                + ")");
148
      }
149
    }
150

151
    // Section 4.1.4: min-append-size validation with exemption for Upload-Complete: ?1
152
    // "This limit does not apply to upload creation requests with no content, or to requests
153
    // completing the upload by including the Upload-Complete: ?1 header field."
154
    Long minAppendSize = uploadStorageService.getMinAppendSize();
3✔
155
    Boolean uploadComplete = StructuredHeaderUtil.parseBoolean(uploadCompleteHeader);
3✔
156
    boolean isCompleteExempt = Boolean.TRUE.equals(uploadComplete);
4✔
157

158
    if (minAppendSize != null && minAppendSize > 0 && !isCompleteExempt) {
9✔
159
      if (contentLength < minAppendSize) {
5✔
160
        throw new MinAppendSizeNotMetException(
7✔
161
            "The request payload size ("
162
                + contentLength
163
                + ") is below the minimum allowed append size ("
164
                + minAppendSize
165
                + ")");
166
      }
167
    }
168

169
    // Section 4.1.3: Validate consistency of Upload-Length if provided in append request
170
    String uploadLengthHeader = request.getHeader(HttpHeader.UPLOAD_LENGTH);
4✔
171
    Long providedLength = StructuredHeaderUtil.parseInteger(uploadLengthHeader);
3✔
172
    if (providedLength != null && uploadInfo.hasLength()) {
5✔
173
      if (!providedLength.equals(uploadInfo.getLength())) {
5✔
174
        throw new InconsistentUploadLengthException(
4✔
175
            "Provided Upload-Length ("
176
                + providedLength
177
                + ") does not match existing upload length ("
178
                + uploadInfo.getLength()
4✔
179
                + ")");
180
      }
181
    }
182
  }
1✔
183
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc