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

tomdesair / tus-java-server / 28704942481

04 Jul 2026 11:35AM UTC coverage: 92.355%. First build
28704942481

Pull #105

github

web-flow
Merge 94b11f46d into e8bbe6ae5
Pull Request #105: feat: Implement IETF Resumable Uploads for HTTP (RUFH) Protocol

866 of 1022 branches covered (84.74%)

Branch coverage included in aggregate %.

443 of 496 new or added lines in 24 files covered. (89.31%)

2263 of 2366 relevant lines covered (95.65%)

6.24 hits per line

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

77.89
/src/main/java/me/desair/tus/server/rufh/HttpProblemDetails.java
1
package me.desair.tus.server.rufh;
2

3
import jakarta.servlet.http.HttpServletResponse;
4
import java.io.IOException;
5
import java.util.Collections;
6
import java.util.LinkedHashMap;
7
import java.util.Map;
8
import java.util.Objects;
9
import me.desair.tus.server.HttpHeader;
10
import me.desair.tus.server.util.TusServletResponse;
11

12
/**
13
 * Value object representing RFC 7807 Problem Details for Resumable Uploads for HTTP (RUFH),
14
 * specified in Section 8 of draft-ietf-httpbis-resumable-upload.
15
 *
16
 * <p>Provides structured data modeling and JSON formatting for problem detail responses.
17
 */
18
public class HttpProblemDetails {
19

20
  private final int status;
21
  private final String type;
22
  private final String title;
23
  private final String detail;
24
  private final Map<String, Object> extraFields;
25

26
  /**
27
   * Construct an immutable RFC 7807 problem details instance.
28
   *
29
   * @param status HTTP response status code
30
   * @param type Problem type URI reference (defaults to "about:blank" if null)
31
   * @param title Short summary of the problem type
32
   * @param detail Human-readable explanation specific to this occurrence of the problem
33
   * @param extraFields Additional extension members (e.g. expected-offset)
34
   */
35
  public HttpProblemDetails(
36
      int status, String type, String title, String detail, Map<String, Object> extraFields) {
2✔
37
    this.status = status;
3✔
38
    this.type = type != null ? type : "about:blank";
6!
39
    this.title = title;
3✔
40
    this.detail = detail;
3✔
41
    this.extraFields =
1✔
42
        extraFields != null
2✔
43
            ? Collections.unmodifiableMap(new LinkedHashMap<>(extraFields))
6✔
44
            : Collections.emptyMap();
2✔
45
  }
1✔
46

47
  /**
48
   * Factory method to create a problem details instance.
49
   *
50
   * @param status HTTP response status code
51
   * @param type Problem type URI reference
52
   * @param title Short summary of the problem type
53
   * @param detail Human-readable explanation
54
   * @param extraFields Additional extension members
55
   * @return A new HttpProblemDetails instance
56
   */
57
  public static HttpProblemDetails of(
58
      int status, String type, String title, String detail, Map<String, Object> extraFields) {
NEW
59
    return new HttpProblemDetails(status, type, title, detail, extraFields);
×
60
  }
61

62
  /**
63
   * Create an Offset Mismatch (409 Conflict) problem details response for RUFH append requests.
64
   *
65
   * <p>Reference: Section 7.1 (Mismatching Offset) of draft-ietf-httpbis-resumable-upload-11: "This
66
   * section defines the 'https://iana.org/assignments/http-problem-types#mismatching-upload-offset'
67
   * problem type."
68
   *
69
   * @param expectedOffset Expected server byte offset
70
   * @param providedOffset Provided Upload-Offset header value
71
   * @return HttpProblemDetails instance configured for offset mismatch
72
   */
73
  public static HttpProblemDetails forOffsetMismatch(long expectedOffset, Long providedOffset) {
74
    Map<String, Object> extra = new LinkedHashMap<>();
4✔
75
    extra.put("expected-offset", expectedOffset);
6✔
76
    return new HttpProblemDetails(
9✔
77
        HttpServletResponse.SC_CONFLICT,
78
        "https://iana.org/assignments/http-problem-types#mismatching-upload-offset",
79
        "Offset Mismatch",
80
        "The provided Upload-Offset does not match the server's current offset",
81
        extra);
82
  }
83

84
  /**
85
   * Create an Upload Completed problem details response.
86
   *
87
   * <p>Reference: Section 7.2 (Completed Upload) of draft-ietf-httpbis-resumable-upload-11: "This
88
   * section defines the 'https://iana.org/assignments/http-problem-types#completed-upload' problem
89
   * type."
90
   *
91
   * @param status HTTP response status code (e.g. 400 Bad Request)
92
   * @return HttpProblemDetails instance configured for completed upload
93
   */
94
  public static HttpProblemDetails forCompletedUpload(int status) {
95
    return new HttpProblemDetails(
9✔
96
        status,
97
        "https://iana.org/assignments/http-problem-types#completed-upload",
98
        "Upload Completed",
99
        "The upload resource is already completed and cannot be modified",
100
        null);
101
  }
102

103
  /**
104
   * Create an Inconsistent Length (400 Bad Request) problem details response.
105
   *
106
   * <p>Reference: Section 7.3 (Inconsistent Length) of draft-ietf-httpbis-resumable-upload-11:
107
   * "This section defines the
108
   * 'https://iana.org/assignments/http-problem-types#inconsistent-upload-length' problem type."
109
   *
110
   * @return HttpProblemDetails instance configured for inconsistent upload length
111
   */
112
  public static HttpProblemDetails forInconsistentLength() {
113
    return new HttpProblemDetails(
9✔
114
        HttpServletResponse.SC_BAD_REQUEST,
115
        "https://iana.org/assignments/http-problem-types#inconsistent-upload-length",
116
        "Inconsistent Upload Length",
117
        "The provided Upload-Length does not match existing metadata",
118
        null);
119
  }
120

121
  /**
122
   * Send problem details response directly to a TusServletResponse.
123
   *
124
   * @param response The servlet response
125
   * @param status HTTP response status code
126
   * @param type Problem type URI reference
127
   * @param title Short summary
128
   * @param detail Human-readable explanation
129
   * @param extraFields Additional extension members
130
   * @throws IOException When writing response fails
131
   */
132
  public static void sendProblemDetails(
133
      TusServletResponse response,
134
      int status,
135
      String type,
136
      String title,
137
      String detail,
138
      Map<String, Object> extraFields)
139
      throws IOException {
NEW
140
    of(status, type, title, detail, extraFields).writeTo(response);
×
NEW
141
  }
×
142

143
  /**
144
   * Helper to send offset mismatch problem details directly.
145
   *
146
   * @param response Servlet response
147
   * @param expectedOffset Expected server offset
148
   * @param providedOffset Provided upload offset
149
   * @throws IOException When writing response fails
150
   */
151
  public static void sendOffsetMismatch(
152
      TusServletResponse response, long expectedOffset, Long providedOffset) throws IOException {
NEW
153
    forOffsetMismatch(expectedOffset, providedOffset).writeTo(response);
×
NEW
154
  }
×
155

156
  /**
157
   * Helper to send completed upload problem details directly.
158
   *
159
   * @param response Servlet response
160
   * @param status HTTP status code
161
   * @throws IOException When writing response fails
162
   */
163
  public static void sendCompletedUpload(TusServletResponse response, int status)
164
      throws IOException {
NEW
165
    forCompletedUpload(status).writeTo(response);
×
NEW
166
  }
×
167

168
  /**
169
   * Helper to send inconsistent length problem details directly.
170
   *
171
   * @param response Servlet response
172
   * @throws IOException When writing response fails
173
   */
174
  public static void sendInconsistentLength(TusServletResponse response) throws IOException {
NEW
175
    forInconsistentLength().writeTo(response);
×
NEW
176
  }
×
177

178
  /**
179
   * Serialize this problem details instance to a JSON string according to RFC 7807.
180
   *
181
   * @return Valid JSON string representation
182
   */
183
  public String toJson() {
184
    Map<String, Object> map = new LinkedHashMap<>();
4✔
185
    map.put("type", type);
6✔
186
    map.put("title", title);
6✔
187
    map.put("status", status);
7✔
188
    if (detail != null) {
3!
189
      map.put("detail", detail);
6✔
190
    }
191
    if (extraFields != null && !extraFields.isEmpty()) {
7!
192
      map.putAll(extraFields);
4✔
193
    }
194
    return formatJson(map);
4✔
195
  }
196

197
  /**
198
   * Write this problem details instance to the HTTP servlet response.
199
   *
200
   * @param response Target HttpServletResponse
201
   * @throws IOException When writing to response stream fails
202
   */
203
  public void writeTo(HttpServletResponse response) throws IOException {
NEW
204
    Objects.requireNonNull(response, "Response cannot be null");
×
NEW
205
    response.setStatus(status);
×
NEW
206
    response.setHeader(HttpHeader.CONTENT_TYPE, HttpHeader.CONTENT_TYPE_PROBLEM_JSON);
×
NEW
207
    response.getWriter().write(toJson());
×
NEW
208
    response.getWriter().flush();
×
NEW
209
  }
×
210

211
  /**
212
   * Write this problem details instance to the TusServletResponse wrapper.
213
   *
214
   * @param response Target TusServletResponse
215
   * @throws IOException When writing to response stream fails
216
   */
217
  public void writeTo(TusServletResponse response) throws IOException {
218
    Objects.requireNonNull(response, "Response cannot be null");
4✔
219
    response.setStatus(status);
4✔
220
    response.setHeader(HttpHeader.CONTENT_TYPE, HttpHeader.CONTENT_TYPE_PROBLEM_JSON);
4✔
221
    response.getWriter().write(toJson());
5✔
222
    response.getWriter().flush();
3✔
223
  }
1✔
224

225
  public int getStatus() {
226
    return status;
3✔
227
  }
228

229
  public String getType() {
230
    return type;
3✔
231
  }
232

233
  public String getTitle() {
234
    return title;
3✔
235
  }
236

237
  public String getDetail() {
238
    return detail;
3✔
239
  }
240

241
  public Map<String, Object> getExtraFields() {
242
    return extraFields;
3✔
243
  }
244

245
  private String formatJson(Map<String, Object> map) {
246
    StringBuilder sb = new StringBuilder();
4✔
247
    sb.append("{");
4✔
248
    boolean first = true;
2✔
249
    for (Map.Entry<String, Object> entry : map.entrySet()) {
11✔
250
      if (!first) {
2✔
251
        sb.append(",");
4✔
252
      }
253
      first = false;
2✔
254
      sb.append("\"").append(escapeJsonString(entry.getKey())).append("\":");
12✔
255
      Object value = entry.getValue();
3✔
256
      if (value instanceof Number || value instanceof Boolean) {
6!
257
        sb.append(value);
5✔
258
      } else {
259
        sb.append("\"").append(escapeJsonString(String.valueOf(value))).append("\"");
11✔
260
      }
261
    }
1✔
262
    sb.append("}");
4✔
263
    return sb.toString();
3✔
264
  }
265

266
  private String escapeJsonString(String value) {
267
    if (value == null) {
2!
NEW
268
      return "";
×
269
    }
270
    return value
4✔
271
        .replace("\\", "\\\\")
3✔
272
        .replace("\"", "\\\"")
3✔
273
        .replace("\b", "\\b")
3✔
274
        .replace("\f", "\\f")
3✔
275
        .replace("\n", "\\n")
3✔
276
        .replace("\r", "\\r")
3✔
277
        .replace("\t", "\\t");
1✔
278
  }
279
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc