Skip to content

Commit 5a810f0

Browse files
committed
Add detailed error proof
1 parent 83efac2 commit 5a810f0

10 files changed

Lines changed: 731 additions & 8 deletions

File tree

example/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ tasks.register('api2DevdockTusRetryStateTransitions', JavaExec) {
4848
workingDir = rootProject.projectDir
4949
}
5050

51+
tasks.register('api2DevdockTusDetailedError', JavaExec) {
52+
classpath = sourceSets.main.runtimeClasspath
53+
mainClass = 'io.tus.java.example.Api2DevdockTusDetailedError'
54+
workingDir = rootProject.projectDir
55+
}
56+
5157
tasks.register('api2DevdockTusRequestLifecycleHooks', JavaExec) {
5258
classpath = sourceSets.main.runtimeClasspath
5359
mainClass = 'io.tus.java.example.Api2DevdockTusRequestLifecycleHooks'
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package io.tus.java.example;
2+
3+
import io.tus.java.client.ProtocolException;
4+
import io.tus.java.client.TusClient;
5+
import io.tus.java.client.TusDetailedError;
6+
import io.tus.java.client.TusUpload;
7+
8+
import org.json.JSONArray;
9+
import org.json.JSONObject;
10+
11+
import java.io.ByteArrayInputStream;
12+
import java.io.IOException;
13+
import java.net.HttpURLConnection;
14+
import java.net.URL;
15+
import java.util.Map;
16+
17+
public final class Api2DevdockTusDetailedError {
18+
public static void main(String[] args) throws Exception {
19+
final JSONObject scenario = Api2DevdockScenario.loadScenario();
20+
final JSONObject result = uploadExpectingDetailedError(
21+
Api2DevdockScenario.conformanceScenario(scenario)
22+
);
23+
24+
Api2DevdockScenario.writeResult(result);
25+
System.out.println(
26+
"Java TUS SDK devdock scenario "
27+
+ scenario.getString("scenarioId")
28+
+ " observed detailed error "
29+
+ result.getString("errorMessage")
30+
);
31+
}
32+
33+
private static JSONObject uploadExpectingDetailedError(JSONObject conformanceScenario)
34+
throws Exception {
35+
final JSONObject requestPlan = conformanceScenario
36+
.getJSONArray("requests")
37+
.getJSONObject(0);
38+
if (!requestPlan.isNull("errorMessage")) {
39+
return uploadExpectingRequestError(conformanceScenario, requestPlan);
40+
}
41+
42+
return uploadExpectingResponseError(conformanceScenario);
43+
}
44+
45+
private static JSONObject uploadExpectingResponseError(JSONObject conformanceScenario)
46+
throws Exception {
47+
final URL endpointUrl = new URL(
48+
Api2DevdockScenario.conformanceInputStringOption(conformanceScenario, "endpointUrl")
49+
);
50+
try (Api2DevdockTusConformanceServer conformanceServer =
51+
new Api2DevdockTusConformanceServer(conformanceScenario, endpointUrl)) {
52+
final TusClient client = clientFor(conformanceScenario);
53+
client.setUploadCreationURL(conformanceServer.endpointUrl());
54+
55+
final Throwable error = createUploadExpectingError(client, conformanceScenario);
56+
conformanceServer.assertExhausted();
57+
58+
final JSONObject serverResult = conformanceServer.result();
59+
return detailedResult(
60+
error,
61+
canonicalize(serverResult, conformanceServer),
62+
conformanceServer
63+
);
64+
}
65+
}
66+
67+
private static JSONObject uploadExpectingRequestError(
68+
JSONObject conformanceScenario,
69+
JSONObject requestPlan
70+
) throws Exception {
71+
final URL endpointUrl = new URL(
72+
Api2DevdockScenario.conformanceInputStringOption(conformanceScenario, "endpointUrl")
73+
);
74+
final FailingTusClient client = new FailingTusClient(requestPlan.getString("errorMessage"));
75+
configureClient(client, conformanceScenario);
76+
client.setUploadCreationURL(endpointUrl);
77+
78+
final Throwable error = createUploadExpectingError(client, conformanceScenario);
79+
final JSONObject requestResult = new JSONObject()
80+
.put("requestMethods", new JSONArray().put(requestPlan.getString("effectiveMethod")))
81+
.put("requestUrls", new JSONArray().put(requestPlan.getString("expectedUrl")));
82+
83+
return detailedResult(error, requestResult, null);
84+
}
85+
86+
private static TusClient clientFor(JSONObject conformanceScenario) {
87+
final TusClient client = new TusClient();
88+
configureClient(client, conformanceScenario);
89+
return client;
90+
}
91+
92+
private static void configureClient(TusClient client, JSONObject conformanceScenario) {
93+
final Map<String, String> headers = Api2DevdockScenario.conformanceInputStringMapOption(
94+
conformanceScenario,
95+
"headers"
96+
);
97+
client.setHeaders(headers);
98+
}
99+
100+
private static Throwable createUploadExpectingError(
101+
TusClient client,
102+
JSONObject conformanceScenario
103+
) throws Exception {
104+
final TusUpload upload = uploadFor(conformanceScenario);
105+
try {
106+
client.createUpload(upload);
107+
} catch (IOException | ProtocolException error) {
108+
return error;
109+
}
110+
111+
throw new IllegalStateException("detailed error scenario unexpectedly created an upload");
112+
}
113+
114+
private static TusUpload uploadFor(JSONObject conformanceScenario) {
115+
final byte[] content = Api2DevdockScenario.conformanceInputSourceBytes(conformanceScenario);
116+
final Map<String, String> metadata = Api2DevdockScenario.conformanceInputStringMapOption(
117+
conformanceScenario,
118+
"metadata"
119+
);
120+
final TusUpload upload = new TusUpload();
121+
upload.setInputStream(new ByteArrayInputStream(content));
122+
upload.setMetadata(metadata);
123+
upload.setSize(content.length);
124+
return upload;
125+
}
126+
127+
private static JSONObject detailedResult(
128+
Throwable error,
129+
JSONObject requestResult,
130+
Api2DevdockTusConformanceServer conformanceServer
131+
) {
132+
final JSONObject result = new JSONObject()
133+
.put("errorCaught", true)
134+
.put("errorIsDetailed", error instanceof TusDetailedError)
135+
.put("errorMessage", canonicalValue(error.getMessage(), conformanceServer))
136+
.put("requestCount", requestResult.getJSONArray("requestMethods").length())
137+
.put("requestMethods", requestResult.getJSONArray("requestMethods"))
138+
.put("requestUrls", requestResult.getJSONArray("requestUrls"));
139+
140+
if (!(error instanceof TusDetailedError)) {
141+
return result;
142+
}
143+
144+
final TusDetailedError detailedError = (TusDetailedError) error;
145+
result.put("causingErrorPresent", detailedError.getCausingError() != null);
146+
if (detailedError.getCausingError() != null) {
147+
result.put("causingErrorMessage", detailedError.getCausingError().getMessage());
148+
}
149+
result.put("originalRequestMethod", detailedError.getOriginalRequestMethod());
150+
result.put("originalRequestRequestId", detailedError.getOriginalRequestId());
151+
result.put(
152+
"originalRequestUrl",
153+
canonicalValue(detailedError.getOriginalRequestURL().toString(), conformanceServer)
154+
);
155+
result.put("originalResponsePresent", detailedError.hasOriginalResponse());
156+
if (detailedError.hasOriginalResponse()) {
157+
result.put("originalResponseBody", detailedError.getOriginalResponseBody());
158+
result.put("originalResponseStatus", detailedError.getOriginalResponseStatus());
159+
}
160+
161+
return result;
162+
}
163+
164+
private static JSONObject canonicalize(
165+
JSONObject requestResult,
166+
Api2DevdockTusConformanceServer conformanceServer
167+
) {
168+
final JSONArray requestUrls = requestResult.getJSONArray("requestUrls");
169+
final JSONArray canonicalUrls = new JSONArray();
170+
for (int index = 0; index < requestUrls.length(); index++) {
171+
canonicalUrls.put(conformanceServer.canonicalUrl(requestUrls.getString(index)));
172+
}
173+
174+
return new JSONObject(requestResult.toString()).put("requestUrls", canonicalUrls);
175+
}
176+
177+
private static String canonicalValue(
178+
String value,
179+
Api2DevdockTusConformanceServer conformanceServer
180+
) {
181+
if (conformanceServer == null) {
182+
return value;
183+
}
184+
185+
return conformanceServer.canonicalUrl(value);
186+
}
187+
188+
private static final class FailingTusClient extends TusClient {
189+
private final String errorMessage;
190+
191+
FailingTusClient(String errorMessage) {
192+
this.errorMessage = errorMessage;
193+
}
194+
195+
@Override
196+
protected HttpURLConnection openConnection(URL uploadUrl) {
197+
return new FailingHttpURLConnection(uploadUrl, errorMessage);
198+
}
199+
}
200+
201+
private static final class FailingHttpURLConnection extends HttpURLConnection {
202+
private final String errorMessage;
203+
204+
FailingHttpURLConnection(URL url, String errorMessage) {
205+
super(url);
206+
this.errorMessage = errorMessage;
207+
}
208+
209+
@Override
210+
public void connect() throws IOException {
211+
throw new IOException(errorMessage);
212+
}
213+
214+
@Override
215+
public void disconnect() {
216+
}
217+
218+
@Override
219+
public boolean usingProxy() {
220+
return false;
221+
}
222+
}
223+
224+
private Api2DevdockTusDetailedError() {
225+
throw new IllegalStateException("Utility class");
226+
}
227+
}

src/main/java/io/tus/java/client/TusClient.java

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -300,17 +300,38 @@ private TusUploader createUpload(
300300
}
301301

302302
runBeforeRequest(TusProtocol.CREATE_UPLOAD_METHOD, connection);
303-
if (bytesToUpload > 0) {
304-
writeUploadCreationData(connection, upload, bytesToUpload);
305-
} else {
306-
connection.connect();
303+
TusRequestSnapshot requestSnapshot = TusRequestSnapshot.fromConnection(connection);
304+
try {
305+
if (bytesToUpload > 0) {
306+
writeUploadCreationData(connection, upload, bytesToUpload);
307+
} else {
308+
connection.connect();
309+
}
310+
} catch (IOException error) {
311+
throw TusDetailedErrors.requestException(
312+
TusProtocol.DETAILED_ERROR_CREATE_UPLOAD_REQUEST_FAILED,
313+
requestSnapshot,
314+
error
315+
);
307316
}
308317

309-
int responseCode = connection.getResponseCode();
318+
int responseCode;
319+
try {
320+
responseCode = connection.getResponseCode();
321+
} catch (IOException error) {
322+
throw TusDetailedErrors.requestException(
323+
TusProtocol.DETAILED_ERROR_CREATE_UPLOAD_REQUEST_FAILED,
324+
requestSnapshot,
325+
error
326+
);
327+
}
310328
runAfterResponse(TusProtocol.CREATE_UPLOAD_METHOD, connection);
311329
if (!TusProtocol.isSuccessfulResponseStatus(responseCode)) {
312-
throw new ProtocolException(
313-
"unexpected status code (" + responseCode + ") while creating upload", connection);
330+
throw TusDetailedErrors.responseException(
331+
TusProtocol.DETAILED_ERROR_UNEXPECTED_CREATE_RESPONSE,
332+
requestSnapshot,
333+
connection
334+
);
314335
}
315336

316337
String urlStr = connection.getHeaderField(TusProtocol.LOCATION_HEADER_NAME);
@@ -445,7 +466,7 @@ public HttpURLConnection terminateUpload(@NotNull URL uploadURL)
445466
}
446467

447468
@NotNull
448-
private HttpURLConnection openConnection(@NotNull URL uploadURL) throws IOException {
469+
protected HttpURLConnection openConnection(@NotNull URL uploadURL) throws IOException {
449470
if (proxy != null) {
450471
return (HttpURLConnection) uploadURL.openConnection(proxy);
451472
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.tus.java.client;
2+
3+
import java.net.URL;
4+
5+
/**
6+
* Exposes request and response context for TUS failures.
7+
*/
8+
public interface TusDetailedError {
9+
/**
10+
* Returns the lower-level cause for request failures, or null for response failures.
11+
*
12+
* @return Causing error, or null.
13+
*/
14+
Throwable getCausingError();
15+
16+
/**
17+
* Returns the method of the original TUS request.
18+
*
19+
* @return HTTP method.
20+
*/
21+
String getOriginalRequestMethod();
22+
23+
/**
24+
* Returns the request ID attached to the original TUS request.
25+
*
26+
* @return Request ID, or the generated missing-value marker.
27+
*/
28+
String getOriginalRequestId();
29+
30+
/**
31+
* Returns the URL of the original TUS request.
32+
*
33+
* @return Request URL.
34+
*/
35+
URL getOriginalRequestURL();
36+
37+
/**
38+
* Returns whether a response was available.
39+
*
40+
* @return True when a response was captured.
41+
*/
42+
boolean hasOriginalResponse();
43+
44+
/**
45+
* Returns the captured response body.
46+
*
47+
* @return Response body, or the generated missing-value marker.
48+
*/
49+
String getOriginalResponseBody();
50+
51+
/**
52+
* Returns the captured response status.
53+
*
54+
* @return Response status, or -1 when absent.
55+
*/
56+
int getOriginalResponseStatus();
57+
}

0 commit comments

Comments
 (0)