1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.volley.cronet; 18 19 import android.content.Context; 20 import android.text.TextUtils; 21 import android.util.Base64; 22 import androidx.annotation.Nullable; 23 import androidx.annotation.VisibleForTesting; 24 import com.android.volley.AuthFailureError; 25 import com.android.volley.Header; 26 import com.android.volley.Request; 27 import com.android.volley.RequestTask; 28 import com.android.volley.VolleyLog; 29 import com.android.volley.toolbox.AsyncHttpStack; 30 import com.android.volley.toolbox.ByteArrayPool; 31 import com.android.volley.toolbox.HttpHeaderParser; 32 import com.android.volley.toolbox.HttpResponse; 33 import com.android.volley.toolbox.PoolingByteArrayOutputStream; 34 import com.android.volley.toolbox.UrlRewriter; 35 import java.io.IOException; 36 import java.io.UnsupportedEncodingException; 37 import java.nio.ByteBuffer; 38 import java.nio.channels.Channels; 39 import java.nio.channels.WritableByteChannel; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.TreeMap; 44 import java.util.concurrent.Executor; 45 import java.util.concurrent.ExecutorService; 46 import org.chromium.net.CronetEngine; 47 import org.chromium.net.CronetException; 48 import org.chromium.net.UploadDataProvider; 49 import org.chromium.net.UploadDataProviders; 50 import org.chromium.net.UrlRequest; 51 import org.chromium.net.UrlRequest.Callback; 52 import org.chromium.net.UrlResponseInfo; 53 54 /** 55 * A {@link AsyncHttpStack} that's based on Cronet's fully asynchronous API for network requests. 56 * 57 * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see 58 * https://github.com/google/volley/wiki/Asynchronous-Volley for more details. 59 */ 60 public class CronetHttpStack extends AsyncHttpStack { 61 62 private final CronetEngine mCronetEngine; 63 private final ByteArrayPool mPool; 64 private final UrlRewriter mUrlRewriter; 65 private final RequestListener mRequestListener; 66 67 // cURL logging support 68 private final boolean mCurlLoggingEnabled; 69 private final CurlCommandLogger mCurlCommandLogger; 70 private final boolean mLogAuthTokensInCurlCommands; 71 CronetHttpStack( CronetEngine cronetEngine, ByteArrayPool pool, UrlRewriter urlRewriter, RequestListener requestListener, boolean curlLoggingEnabled, CurlCommandLogger curlCommandLogger, boolean logAuthTokensInCurlCommands)72 private CronetHttpStack( 73 CronetEngine cronetEngine, 74 ByteArrayPool pool, 75 UrlRewriter urlRewriter, 76 RequestListener requestListener, 77 boolean curlLoggingEnabled, 78 CurlCommandLogger curlCommandLogger, 79 boolean logAuthTokensInCurlCommands) { 80 mCronetEngine = cronetEngine; 81 mPool = pool; 82 mUrlRewriter = urlRewriter; 83 mRequestListener = requestListener; 84 mCurlLoggingEnabled = curlLoggingEnabled; 85 mCurlCommandLogger = curlCommandLogger; 86 mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands; 87 88 mRequestListener.initialize(this); 89 } 90 91 @Override executeRequest( final Request<?> request, final Map<String, String> additionalHeaders, final OnRequestComplete callback)92 public void executeRequest( 93 final Request<?> request, 94 final Map<String, String> additionalHeaders, 95 final OnRequestComplete callback) { 96 if (getBlockingExecutor() == null || getNonBlockingExecutor() == null) { 97 throw new IllegalStateException("Must set blocking and non-blocking executors"); 98 } 99 final Callback urlCallback = 100 new Callback() { 101 PoolingByteArrayOutputStream bytesReceived = null; 102 WritableByteChannel receiveChannel = null; 103 104 @Override 105 public void onRedirectReceived( 106 UrlRequest urlRequest, 107 UrlResponseInfo urlResponseInfo, 108 String newLocationUrl) { 109 urlRequest.followRedirect(); 110 } 111 112 @Override 113 public void onResponseStarted( 114 UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) { 115 bytesReceived = 116 new PoolingByteArrayOutputStream( 117 mPool, getContentLength(urlResponseInfo)); 118 receiveChannel = Channels.newChannel(bytesReceived); 119 urlRequest.read(ByteBuffer.allocateDirect(1024)); 120 } 121 122 @Override 123 public void onReadCompleted( 124 UrlRequest urlRequest, 125 UrlResponseInfo urlResponseInfo, 126 ByteBuffer byteBuffer) { 127 byteBuffer.flip(); 128 try { 129 receiveChannel.write(byteBuffer); 130 byteBuffer.clear(); 131 urlRequest.read(byteBuffer); 132 } catch (IOException e) { 133 urlRequest.cancel(); 134 callback.onError(e); 135 } 136 } 137 138 @Override 139 public void onSucceeded( 140 UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) { 141 List<Header> headers = getHeaders(urlResponseInfo.getAllHeadersAsList()); 142 HttpResponse response = 143 new HttpResponse( 144 urlResponseInfo.getHttpStatusCode(), 145 headers, 146 bytesReceived.toByteArray()); 147 callback.onSuccess(response); 148 } 149 150 @Override 151 public void onFailed( 152 UrlRequest urlRequest, 153 UrlResponseInfo urlResponseInfo, 154 CronetException e) { 155 callback.onError(e); 156 } 157 }; 158 159 String url = request.getUrl(); 160 String rewritten = mUrlRewriter.rewriteUrl(url); 161 if (rewritten == null) { 162 callback.onError(new IOException("URL blocked by rewriter: " + url)); 163 return; 164 } 165 url = rewritten; 166 167 // We can call allowDirectExecutor here and run directly on the network thread, since all 168 // the callbacks are non-blocking. 169 final UrlRequest.Builder builder = 170 mCronetEngine 171 .newUrlRequestBuilder(url, urlCallback, getNonBlockingExecutor()) 172 .allowDirectExecutor() 173 .disableCache() 174 .setPriority(getPriority(request)); 175 // request.getHeaders() may be blocking, so submit it to the blocking executor. 176 getBlockingExecutor() 177 .execute( 178 new SetUpRequestTask<>(request, url, builder, additionalHeaders, callback)); 179 } 180 181 private class SetUpRequestTask<T> extends RequestTask<T> { 182 UrlRequest.Builder builder; 183 String url; 184 Map<String, String> additionalHeaders; 185 OnRequestComplete callback; 186 Request<T> request; 187 SetUpRequestTask( Request<T> request, String url, UrlRequest.Builder builder, Map<String, String> additionalHeaders, OnRequestComplete callback)188 SetUpRequestTask( 189 Request<T> request, 190 String url, 191 UrlRequest.Builder builder, 192 Map<String, String> additionalHeaders, 193 OnRequestComplete callback) { 194 super(request); 195 // Note that this URL may be different from Request#getUrl() due to the UrlRewriter. 196 this.url = url; 197 this.builder = builder; 198 this.additionalHeaders = additionalHeaders; 199 this.callback = callback; 200 this.request = request; 201 } 202 203 @Override run()204 public void run() { 205 try { 206 mRequestListener.onRequestPrepared(request, builder); 207 CurlLoggedRequestParameters requestParameters = new CurlLoggedRequestParameters(); 208 setHttpMethod(requestParameters, request); 209 setRequestHeaders(requestParameters, request, additionalHeaders); 210 requestParameters.applyToRequest(builder, getNonBlockingExecutor()); 211 UrlRequest urlRequest = builder.build(); 212 if (mCurlLoggingEnabled) { 213 mCurlCommandLogger.logCurlCommand(generateCurlCommand(url, requestParameters)); 214 } 215 urlRequest.start(); 216 } catch (AuthFailureError authFailureError) { 217 callback.onAuthError(authFailureError); 218 } 219 } 220 } 221 222 @VisibleForTesting getHeaders(List<Map.Entry<String, String>> headersList)223 public static List<Header> getHeaders(List<Map.Entry<String, String>> headersList) { 224 List<Header> headers = new ArrayList<>(); 225 for (Map.Entry<String, String> header : headersList) { 226 headers.add(new Header(header.getKey(), header.getValue())); 227 } 228 return headers; 229 } 230 231 /** Sets the connection parameters for the UrlRequest */ setHttpMethod(CurlLoggedRequestParameters requestParameters, Request<?> request)232 private void setHttpMethod(CurlLoggedRequestParameters requestParameters, Request<?> request) 233 throws AuthFailureError { 234 switch (request.getMethod()) { 235 case Request.Method.DEPRECATED_GET_OR_POST: 236 // This is the deprecated way that needs to be handled for backwards compatibility. 237 // If the request's post body is null, then the assumption is that the request is 238 // GET. Otherwise, it is assumed that the request is a POST. 239 byte[] postBody = request.getPostBody(); 240 if (postBody != null) { 241 requestParameters.setHttpMethod("POST"); 242 addBodyIfExists(requestParameters, request.getPostBodyContentType(), postBody); 243 } else { 244 requestParameters.setHttpMethod("GET"); 245 } 246 break; 247 case Request.Method.GET: 248 // Not necessary to set the request method because connection defaults to GET but 249 // being explicit here. 250 requestParameters.setHttpMethod("GET"); 251 break; 252 case Request.Method.DELETE: 253 requestParameters.setHttpMethod("DELETE"); 254 break; 255 case Request.Method.POST: 256 requestParameters.setHttpMethod("POST"); 257 addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody()); 258 break; 259 case Request.Method.PUT: 260 requestParameters.setHttpMethod("PUT"); 261 addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody()); 262 break; 263 case Request.Method.HEAD: 264 requestParameters.setHttpMethod("HEAD"); 265 break; 266 case Request.Method.OPTIONS: 267 requestParameters.setHttpMethod("OPTIONS"); 268 break; 269 case Request.Method.TRACE: 270 requestParameters.setHttpMethod("TRACE"); 271 break; 272 case Request.Method.PATCH: 273 requestParameters.setHttpMethod("PATCH"); 274 addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody()); 275 break; 276 default: 277 throw new IllegalStateException("Unknown method type."); 278 } 279 } 280 281 /** 282 * Sets the request headers for the UrlRequest. 283 * 284 * @param requestParameters parameters that we are adding the request headers to 285 * @param request to get the headers from 286 * @param additionalHeaders for the UrlRequest 287 * @throws AuthFailureError is thrown if Request#getHeaders throws ones 288 */ setRequestHeaders( CurlLoggedRequestParameters requestParameters, Request<?> request, Map<String, String> additionalHeaders)289 private void setRequestHeaders( 290 CurlLoggedRequestParameters requestParameters, 291 Request<?> request, 292 Map<String, String> additionalHeaders) 293 throws AuthFailureError { 294 requestParameters.putAllHeaders(additionalHeaders); 295 // Request.getHeaders() takes precedence over the given additional (cache) headers). 296 requestParameters.putAllHeaders(request.getHeaders()); 297 } 298 299 /** Sets the UploadDataProvider of the UrlRequest.Builder */ addBodyIfExists( CurlLoggedRequestParameters requestParameters, String contentType, @Nullable byte[] body)300 private void addBodyIfExists( 301 CurlLoggedRequestParameters requestParameters, 302 String contentType, 303 @Nullable byte[] body) { 304 requestParameters.setBody(contentType, body); 305 } 306 307 /** Helper method that maps Volley's request priority to Cronet's */ getPriority(Request<?> request)308 private int getPriority(Request<?> request) { 309 switch (request.getPriority()) { 310 case LOW: 311 return UrlRequest.Builder.REQUEST_PRIORITY_LOW; 312 case HIGH: 313 case IMMEDIATE: 314 return UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST; 315 case NORMAL: 316 default: 317 return UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM; 318 } 319 } 320 getContentLength(UrlResponseInfo urlResponseInfo)321 private int getContentLength(UrlResponseInfo urlResponseInfo) { 322 List<String> content = urlResponseInfo.getAllHeaders().get("Content-Length"); 323 if (content == null) { 324 return 1024; 325 } else { 326 return Integer.parseInt(content.get(0)); 327 } 328 } 329 generateCurlCommand(String url, CurlLoggedRequestParameters requestParameters)330 private String generateCurlCommand(String url, CurlLoggedRequestParameters requestParameters) { 331 StringBuilder builder = new StringBuilder("curl "); 332 333 // HTTP method 334 builder.append("-X ").append(requestParameters.getHttpMethod()).append(" "); 335 336 // Request headers 337 for (Map.Entry<String, String> header : requestParameters.getHeaders().entrySet()) { 338 builder.append("--header \"").append(header.getKey()).append(": "); 339 if (!mLogAuthTokensInCurlCommands 340 && ("Authorization".equals(header.getKey()) 341 || "Cookie".equals(header.getKey()))) { 342 builder.append("[REDACTED]"); 343 } else { 344 builder.append(header.getValue()); 345 } 346 builder.append("\" "); 347 } 348 349 // URL 350 builder.append("\"").append(url).append("\""); 351 352 // Request body (if any) 353 if (requestParameters.getBody() != null) { 354 if (requestParameters.getBody().length >= 1024) { 355 builder.append(" [REQUEST BODY TOO LARGE TO INCLUDE]"); 356 } else if (isBinaryContentForLogging(requestParameters)) { 357 String base64 = Base64.encodeToString(requestParameters.getBody(), Base64.NO_WRAP); 358 builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; ") 359 .append(" --data-binary @/tmp/$$.bin"); 360 } else { 361 // Just assume the request body is UTF-8 since this is for debugging. 362 try { 363 builder.append(" --data-ascii \"") 364 .append(new String(requestParameters.getBody(), "UTF-8")) 365 .append("\""); 366 } catch (UnsupportedEncodingException e) { 367 throw new RuntimeException("Could not encode to UTF-8", e); 368 } 369 } 370 } 371 372 return builder.toString(); 373 } 374 375 /** Rough heuristic to determine whether the request body is binary, for logging purposes. */ isBinaryContentForLogging(CurlLoggedRequestParameters requestParameters)376 private boolean isBinaryContentForLogging(CurlLoggedRequestParameters requestParameters) { 377 // Check to see if the content is gzip compressed - this means it should be treated as 378 // binary content regardless of the content type. 379 String contentEncoding = requestParameters.getHeaders().get("Content-Encoding"); 380 if (contentEncoding != null) { 381 String[] encodings = TextUtils.split(contentEncoding, ","); 382 for (String encoding : encodings) { 383 if ("gzip".equals(encoding.trim())) { 384 return true; 385 } 386 } 387 } 388 389 // If the content type is a known text type, treat it as text content. 390 String contentType = requestParameters.getHeaders().get("Content-Type"); 391 if (contentType != null) { 392 return !contentType.startsWith("text/") 393 && !contentType.startsWith("application/xml") 394 && !contentType.startsWith("application/json"); 395 } 396 397 // Otherwise, assume it is binary content. 398 return true; 399 } 400 401 /** 402 * Builder is used to build an instance of {@link CronetHttpStack} from values configured by the 403 * setters. 404 */ 405 public static class Builder { 406 private static final int DEFAULT_POOL_SIZE = 4096; 407 private CronetEngine mCronetEngine; 408 private final Context context; 409 private ByteArrayPool mPool; 410 private UrlRewriter mUrlRewriter; 411 private RequestListener mRequestListener; 412 private boolean mCurlLoggingEnabled; 413 private CurlCommandLogger mCurlCommandLogger; 414 private boolean mLogAuthTokensInCurlCommands; 415 Builder(Context context)416 public Builder(Context context) { 417 this.context = context; 418 } 419 420 /** Sets the CronetEngine to be used. Defaults to a vanialla CronetEngine. */ setCronetEngine(CronetEngine engine)421 public Builder setCronetEngine(CronetEngine engine) { 422 mCronetEngine = engine; 423 return this; 424 } 425 426 /** Sets the ByteArrayPool to be used. Defaults to a new pool with 4096 bytes. */ setPool(ByteArrayPool pool)427 public Builder setPool(ByteArrayPool pool) { 428 mPool = pool; 429 return this; 430 } 431 432 /** Sets the UrlRewriter to be used. Default is to return the original string. */ setUrlRewriter(UrlRewriter urlRewriter)433 public Builder setUrlRewriter(UrlRewriter urlRewriter) { 434 mUrlRewriter = urlRewriter; 435 return this; 436 } 437 438 /** Set the optional RequestListener to be used. */ setRequestListener(RequestListener requestListener)439 public Builder setRequestListener(RequestListener requestListener) { 440 mRequestListener = requestListener; 441 return this; 442 } 443 444 /** 445 * Sets whether cURL logging should be enabled for debugging purposes. 446 * 447 * <p>When enabled, for each request dispatched to the network, a roughly-equivalent cURL 448 * command will be logged to logcat. 449 * 450 * <p>The command may be missing some headers that are added by Cronet automatically, and 451 * the full request body may not be included if it is too large. To inspect the full 452 * requests and responses, see {@code CronetEngine#startNetLogToFile}. 453 * 454 * <p>WARNING: This is only intended for debugging purposes and should never be enabled on 455 * production devices. 456 * 457 * @see #setCurlCommandLogger(CurlCommandLogger) 458 * @see #setLogAuthTokensInCurlCommands(boolean) 459 */ setCurlLoggingEnabled(boolean curlLoggingEnabled)460 public Builder setCurlLoggingEnabled(boolean curlLoggingEnabled) { 461 mCurlLoggingEnabled = curlLoggingEnabled; 462 return this; 463 } 464 465 /** 466 * Sets the function used to log cURL commands. 467 * 468 * <p>Allows customization of the logging performed when cURL logging is enabled. 469 * 470 * <p>By default, when cURL logging is enabled, cURL commands are logged using {@link 471 * VolleyLog#v}, e.g. at the verbose log level with the same log tag used by the rest of 472 * Volley. This function may optionally be invoked to provide a custom logger. 473 * 474 * @see #setCurlLoggingEnabled(boolean) 475 */ setCurlCommandLogger(CurlCommandLogger curlCommandLogger)476 public Builder setCurlCommandLogger(CurlCommandLogger curlCommandLogger) { 477 mCurlCommandLogger = curlCommandLogger; 478 return this; 479 } 480 481 /** 482 * Sets whether to log known auth tokens in cURL commands, or redact them. 483 * 484 * <p>By default, headers which may contain auth tokens (e.g. Authorization or Cookie) will 485 * have their values redacted. Passing true to this method will disable this redaction and 486 * log the values of these headers. 487 * 488 * <p>This heuristic is not perfect; tokens that are logged in unknown headers, or in the 489 * request body itself, will not be redacted as they cannot be detected generically. 490 * 491 * @see #setCurlLoggingEnabled(boolean) 492 */ setLogAuthTokensInCurlCommands(boolean logAuthTokensInCurlCommands)493 public Builder setLogAuthTokensInCurlCommands(boolean logAuthTokensInCurlCommands) { 494 mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands; 495 return this; 496 } 497 build()498 public CronetHttpStack build() { 499 if (mCronetEngine == null) { 500 mCronetEngine = new CronetEngine.Builder(context).build(); 501 } 502 if (mUrlRewriter == null) { 503 mUrlRewriter = 504 new UrlRewriter() { 505 @Override 506 public String rewriteUrl(String originalUrl) { 507 return originalUrl; 508 } 509 }; 510 } 511 if (mRequestListener == null) { 512 mRequestListener = new RequestListener() {}; 513 } 514 if (mPool == null) { 515 mPool = new ByteArrayPool(DEFAULT_POOL_SIZE); 516 } 517 if (mCurlCommandLogger == null) { 518 mCurlCommandLogger = 519 new CurlCommandLogger() { 520 @Override 521 public void logCurlCommand(String curlCommand) { 522 VolleyLog.v(curlCommand); 523 } 524 }; 525 } 526 return new CronetHttpStack( 527 mCronetEngine, 528 mPool, 529 mUrlRewriter, 530 mRequestListener, 531 mCurlLoggingEnabled, 532 mCurlCommandLogger, 533 mLogAuthTokensInCurlCommands); 534 } 535 } 536 537 /** Callback interface allowing clients to intercept different parts of the request flow. */ 538 public abstract static class RequestListener { 539 private CronetHttpStack mStack; 540 initialize(CronetHttpStack stack)541 void initialize(CronetHttpStack stack) { 542 mStack = stack; 543 } 544 545 /** 546 * Called when a request is prepared and about to be sent over the network. 547 * 548 * <p>Clients may use this callback to customize UrlRequests before they are dispatched, 549 * e.g. to enable socket tagging or request finished listeners. 550 */ onRequestPrepared(Request<?> request, UrlRequest.Builder requestBuilder)551 public void onRequestPrepared(Request<?> request, UrlRequest.Builder requestBuilder) {} 552 553 /** @see AsyncHttpStack#getNonBlockingExecutor() */ getNonBlockingExecutor()554 protected Executor getNonBlockingExecutor() { 555 return mStack.getNonBlockingExecutor(); 556 } 557 558 /** @see AsyncHttpStack#getBlockingExecutor() */ getBlockingExecutor()559 protected Executor getBlockingExecutor() { 560 return mStack.getBlockingExecutor(); 561 } 562 } 563 564 /** 565 * Interface for logging cURL commands for requests. 566 * 567 * @see Builder#setCurlCommandLogger(CurlCommandLogger) 568 */ 569 public interface CurlCommandLogger { 570 /** Log the given cURL command. */ logCurlCommand(String curlCommand)571 void logCurlCommand(String curlCommand); 572 } 573 574 /** 575 * Internal container class for request parameters that impact logged cURL commands. 576 * 577 * <p>When cURL logging is enabled, an equivalent cURL command to a given request must be 578 * generated and logged. However, the Cronet UrlRequest object is write-only. So, we write any 579 * relevant parameters into this read-write container so they can be referenced when generating 580 * the cURL command (if needed) and then merged into the UrlRequest. 581 */ 582 private static class CurlLoggedRequestParameters { 583 private final TreeMap<String, String> mHeaders = 584 new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 585 private String mHttpMethod; 586 @Nullable private byte[] mBody; 587 588 /** 589 * Return the headers to be used for the request. 590 * 591 * <p>The returned map is case-insensitive. 592 */ getHeaders()593 TreeMap<String, String> getHeaders() { 594 return mHeaders; 595 } 596 597 /** Apply all the headers in the given map to the request. */ putAllHeaders(Map<String, String> headers)598 void putAllHeaders(Map<String, String> headers) { 599 mHeaders.putAll(headers); 600 } 601 getHttpMethod()602 String getHttpMethod() { 603 return mHttpMethod; 604 } 605 setHttpMethod(String httpMethod)606 void setHttpMethod(String httpMethod) { 607 mHttpMethod = httpMethod; 608 } 609 610 @Nullable getBody()611 byte[] getBody() { 612 return mBody; 613 } 614 setBody(String contentType, @Nullable byte[] body)615 void setBody(String contentType, @Nullable byte[] body) { 616 mBody = body; 617 if (body != null && !mHeaders.containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) { 618 // Set the content-type unless it was already set (by Request#getHeaders). 619 mHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, contentType); 620 } 621 } 622 applyToRequest(UrlRequest.Builder builder, ExecutorService nonBlockingExecutor)623 void applyToRequest(UrlRequest.Builder builder, ExecutorService nonBlockingExecutor) { 624 for (Map.Entry<String, String> header : mHeaders.entrySet()) { 625 builder.addHeader(header.getKey(), header.getValue()); 626 } 627 builder.setHttpMethod(mHttpMethod); 628 if (mBody != null) { 629 UploadDataProvider dataProvider = UploadDataProviders.create(mBody); 630 builder.setUploadDataProvider(dataProvider, nonBlockingExecutor); 631 } 632 } 633 } 634 } 635