2020import org .apache .hc .client5 .http .classic .methods .HttpUriRequest ;
2121import org .apache .hc .client5 .http .impl .classic .CloseableHttpResponse ;
2222import org .apache .hc .core5 .http .Header ;
23+ import org .apache .hc .core5 .http .HttpHeaders ;
24+ import org .apache .hc .core5 .http .ProtocolException ;
2325import org .apache .hc .core5 .net .URIBuilder ;
2426import org .json .JSONObject ;
2527import org .json .JSONTokener ;
28+ import org .labkey .remoteapi .query .SelectRowsCommand ;
2629
2730import java .io .BufferedReader ;
2831import java .io .ByteArrayInputStream ;
4447/**
4548 * Abstract base class for all API commands. Developers interact with concrete classes that
4649 * extend this class. For example, to select data, create an instance of
47- * {@link org.labkey.remoteapi.query. SelectRowsCommand}, which provides helpful methods for
50+ * {@link SelectRowsCommand}, which provides helpful methods for
4851 * setting options and obtaining specific result types.
4952 * <p>
5053 * If a developer wishes to invoke actions that are not yet supported with a specialized class
@@ -283,6 +286,19 @@ public String getText() throws IOException
283286 }
284287 }
285288
289+ public String getHeaderValue (String name )
290+ {
291+ Header header = null ;
292+ try
293+ {
294+ header = _httpResponse .getHeader (name );
295+ }
296+ catch (ProtocolException ignored )
297+ {
298+ }
299+ return null == header ? null : header .getValue ();
300+ }
301+
286302 // Caller is responsible for closing the response
287303 @ Override
288304 public void close () throws IOException
@@ -291,27 +307,39 @@ public void close() throws IOException
291307 }
292308 }
293309
294- /* NOTE: Internal experimental API for handling streaming commands. */
295310 protected Response _execute (Connection connection , String folderPath ) throws CommandException , IOException
296311 {
297312 assert null != getControllerName () : "You must set the controller name before executing the command!" ;
298313 assert null != getActionName () : "You must set the action name before executing the command!" ;
299- CloseableHttpResponse httpResponse ;
300314
301- final HttpUriRequest request ;
302315 try
303316 {
304317 //construct and initialize the HttpUriRequest
305- request = getHttpRequest (connection , folderPath );
306- LogFactory .getLog (Command .class ).info ("Requesting URL: " + request .getRequestUri ());
307-
308- //execute the request
309- httpResponse = connection .executeRequest (request , getTimeout ());
318+ final HttpUriRequest request = getHttpRequest (connection , folderPath );
319+ try
320+ {
321+ return executeRequest (connection , request );
322+ }
323+ catch (CommandException e )
324+ {
325+ if (connection .getCredentialsProvider ().shouldRetryRequest (e , request ))
326+ return executeRequest (connection , request );
327+ else
328+ throw e ;
329+ }
310330 }
311331 catch (URISyntaxException | AuthenticationException e )
312332 {
313333 throw new CommandException (e .getMessage ());
314334 }
335+ }
336+
337+ private Response executeRequest (Connection connection , HttpUriRequest request ) throws AuthenticationException , IOException , CommandException
338+ {
339+ LogFactory .getLog (Command .class ).info ("Requesting URL: " + request .getRequestUri ());
340+
341+ //execute the request
342+ CloseableHttpResponse httpResponse = connection .executeRequest (request , getTimeout ());
315343
316344 //get the content-type header
317345 Header contentTypeHeader = httpResponse .getFirstHeader ("Content-Type" );
@@ -322,10 +350,11 @@ protected Response _execute(Connection connection, String folderPath) throws Com
322350
323351 Response response = new Response (httpResponse , contentType , contentLength );
324352 checkThrowError (response );
353+
325354 return response ;
326355 }
327356
328- protected void checkThrowError (Response response ) throws IOException , CommandException
357+ private void checkThrowError (Response response ) throws IOException , CommandException
329358 {
330359 int status = response .getStatusCode ();
331360
@@ -352,6 +381,8 @@ private void throwError(Response r, boolean throwByDefault) throws IOException,
352381 //use the status text as the message by default
353382 String message = null != r .getStatusText () ? r .getStatusText () : "(no status text)" ;
354383
384+ String authHeaderValue = r .getHeaderValue (HttpHeaders .WWW_AUTHENTICATE );
385+
355386 // This buffers the entire response in memory, which seems OK for API error responses.
356387 String responseText = r .getText ();
357388 JSONObject json = null ;
@@ -370,15 +401,15 @@ private void throwError(Response r, boolean throwByDefault) throws IOException,
370401 message = json .getString ("exception" );
371402
372403 if ("org.labkey.api.action.ApiVersionException" .equals (json .opt ("exceptionClass" )))
373- throw new ApiVersionException (message , r .getStatusCode (), json , responseText , contentType );
404+ throw new ApiVersionException (message , r .getStatusCode (), json , responseText , contentType , authHeaderValue );
374405
375- throw new CommandException (message , r .getStatusCode (), json , responseText , contentType );
406+ throw new CommandException (message , r .getStatusCode (), json , responseText , contentType , authHeaderValue );
376407 }
377408 }
378409 }
379410
380411 if (throwByDefault )
381- throw new CommandException (message , r .getStatusCode (), json , responseText , contentType );
412+ throw new CommandException (message , r .getStatusCode (), json , responseText , contentType , authHeaderValue );
382413
383414 // If we didn't encounter an exception property on the json object, save the fully consumed text and parsed json on the Response object
384415 r ._json = json ;
0 commit comments