private Callback<String> routerPutBlobCallback(BlobInfo blobInfo) {
      return buildCallback(frontendMetrics.putRouterPutBlobMetrics, blobId -> {
        restRequest.getMetricsTracker().setBytesTransferred(restRequest.getBytesReceived());
        restResponseChannel.setHeader(RestUtils.Headers.BLOB_SIZE, restRequest.getBlobBytesReceived());
        restResponseChannel.setHeader(RestUtils.Headers.LOCATION, blobId);
        String blobIdClean = stripPrefixAndExtension(blobId);
        if (blobInfo.getBlobProperties().getTimeToLiveInSeconds() == Utils.Infinite_Time) {
          // Do ttl update with retryExecutor. Use the blob ID returned from the router instead of the converted ID
          // since the converted ID may be changed by the ID converter.
          String serviceId = blobInfo.getBlobProperties().getServiceId();
          retryExecutor.runWithRetries(retryPolicy,
              callback -> router.updateBlobTtl(restRequest, blobIdClean, serviceId, Utils.Infinite_Time, callback,
                  QuotaUtils.buildQuotaChargeCallback(restRequest, quotaManager, false)),
              this::isRetriable, routerTtlUpdateCallbackForPut(blobInfo));
        } else {
          if (RestUtils.isDatasetVersionQueryEnabled(restRequest.getArgs())) {
            //Make sure to process response after delete finished
            updateVersionStateAndDeleteDatasetVersionOutOfRetentionCount(
                deleteDatasetVersionOutOfRetentionCallback(blobInfo));
          } else {
            securityService.processResponse(restRequest, restResponseChannel, blobInfo,
                securityProcessResponseCallback());
          }
        }
      }, uri, LOGGER, deleteDatasetCallback);
    }
After {@link Router#putBlob} finishes, call {@link IdConverter#convert} to store the mapping between blobName and
blobId.
@param blobInfo the {@link BlobInfo} to use for security checks.
@return a {@link Callback} to be used with {@link Router#putBlob}.
ambry-frontend/src/main/java/com/github/ambry/frontend/NamedBlobPutHandler.java