From 20682c6d9b74ba88c23f35cf123e04553068b424 Mon Sep 17 00:00:00 2001 From: "promptless[bot]" Date: Mon, 26 Oct 2026 15:13:45 +0000 Subject: [PATCH 1/2] SK-3253: Document automatic retry on 401 for expired bearer tokens --- docs/api_reference.md | 3 +- flowvault/README.md | 11 ++ skyvault/README.md | 108 ++++++++---------- .../BearerTokenExpiryExample.java | 90 ++++++--------- 4 files changed, 98 insertions(+), 114 deletions(-) diff --git a/docs/api_reference.md b/docs/api_reference.md index 8f5716a5..5e5570b4 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -733,4 +733,5 @@ Response: a `List` of objects, each with: | Method | Return type | Description | |--------|-------------|-------------| -| `Token.isExpired(String token)` | `boolean` | Returns `true` if the token is `null` or has expired. Use before every API call to avoid mid-flight expiry. | +| `Token.isExpired(String token)` | `boolean` | Returns `true` if the token is `null` or has expired. Use it before every API call when you manage tokens yourself. | +| `Token.isExpiringWithin(String token, long windowSeconds)` | `boolean` | Returns `true` if the token has expired or expires within `windowSeconds`. Returns `false` if the token is `null`, empty, malformed, or has no `exp` claim. | diff --git a/flowvault/README.md b/flowvault/README.md index c6d78f26..62b588c5 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -20,6 +20,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope - [Authenticate](#authenticate) - [Credential types](#credential-types) - [Where credentials can be set](#where-credentials-can-be-set) + - [Bearer token expiry during a call](#bearer-token-expiry-during-a-call) - [Generate a bearer token](#generate-a-bearer-token) - [Generate bearer tokens with context](#generate-bearer-tokens-with-context) - [Generate scoped bearer tokens](#generate-scoped-bearer-tokens) @@ -168,6 +169,16 @@ Credentials resolve **most specific first**: If none of the three yields credentials, the call fails with a `SkyflowException`. +## Bearer token expiry during a call + +The SDK checks the bearer token once, at the start of each call, and a bulk call can then send many batch requests with that token. If the token expires partway through, Skyflow rejects the requests that follow with HTTP `401`. + +The SDK recovers from this on its own when it generates tokens for you. That is the case with `setPath`, `setCredentialsString`, or the `SKYFLOW_CREDENTIALS` environment variable. If a request fails with `401` and its token expires within 60 seconds, the SDK generates a new token. It then sends that request once more. The remaining batches use the new token, so a bulk call that runs past the token's expiry still completes. Concurrent requests that fail on the same token share one new token. The SDK retries each request once and returns the response to that retry. It returns the original `401` instead when it can't generate a new token, or when the token wasn't close to expiry. + +This retry is always on and has no settings. It runs inside each transport attempt, so the overall `timeout` and the `maxRetries` policy in [Timeouts and retries](#timeouts-and-retries) still bound the whole call. + +A bearer token you provide with `setToken`, or an API key, gets no retry: the SDK returns the `401` as is. To keep long-running calls from failing on an expired token, let the SDK generate tokens, or generate a new token yourself before it expires. + ## Generate a bearer token If you would rather manage tokens yourself, the service-account utilities ship inside `skyflow-flowvault-java`. This section covers each of them: diff --git a/skyvault/README.md b/skyvault/README.md index e996553a..69518f54 100644 --- a/skyvault/README.md +++ b/skyvault/README.md @@ -2855,101 +2855,91 @@ Notes: - By default, the TTL value is set to 60 seconds. ## Bearer token expiry edge case -When you use bearer tokens for authentication and API requests in SDKs, there's the potential for a token to expire after the token is verified as valid but before the actual API call is made, causing the request to fail unexpectedly due to the token's expiration. An error from this edge case would look something like this: + +A bearer token can expire after the SDK checks it but before a request that uses it reaches Skyflow. The request then fails with HTTP `401` and an error like this one: ```txt message: Authentication failed. Bearer token is expired. Use a valid bearer token. See https://docs.skyflow.com/api-authentication/ ``` -If you encounter this kind of error, retry the request. During the retry, the SDK detects that the previous bearer token has expired and generates a new one for the current and subsequent requests. +Vault and Detect calls handle this case themselves when the SDK generates tokens for you. That is the case with `setPath`, `setCredentialsString`, or the `SKYFLOW_CREDENTIALS` environment variable. If a request fails with `401` and its token expires within 60 seconds, the SDK generates a new token. It then sends that request once more. Later requests in the same call use the new token. A `401` for any other reason is returned without a retry. + +Handle the `401` yourself in these cases: + +- **Connection calls.** `invoke` on a connection returns the `401` without a retry. Retry the call: at the start of the retry, the SDK sees that the token has expired and generates a new one. +- **Bearer tokens you provide with `setToken`.** The SDK can't replace your token, so it returns the `401`. Generate a new token, update the credentials (see [Credentials and log level management](#credentials-and-log-level-management)), and then retry the call. -#### [Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java): +#### [Example](https://github.com/skyflowapi/skyflow-java/blob/main/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java): ```java package com.example.serviceaccount; import com.skyflow.Skyflow; +import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; -import com.skyflow.config.VaultConfig; -import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; -import com.skyflow.enums.RedactionType; +import com.skyflow.enums.RequestMethod; import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.tokens.DetokenizeRequest; -import com.skyflow.vault.tokens.DetokenizeResponse; -import io.github.cdimascio.dotenv.Dotenv; -import java.util.ArrayList; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import com.skyflow.vault.connection.InvokeConnectionResponse; + +import java.util.HashMap; +import java.util.Map; /** - * This example demonstrates how to configure and use the Skyflow SDK - * to detokenize sensitive data stored in a Skyflow vault. - * It includes setting up credentials, configuring the vault, and - * making a detokenization request. The code also implements a retry - * mechanism to handle unauthorized access errors (HTTP 401). + * This example demonstrates how to handle a bearer token that expires while a + * connection request is in flight. Vault and Detect calls retry once on their own + * when this happens, but connection calls return the 401 error, so this example + * retries the connection call once when it fails with HTTP 401. */ -public class DetokenizeExample { +public class BearerTokenExpiryExample { public static void main(String[] args) { try { - // Setting up credentials for accessing the Skyflow vault - Credentials vaultCredentials = new Credentials(); - vaultCredentials.setCredentialsString(""); - - // Configuring the Skyflow vault with necessary details - VaultConfig vaultConfig = new VaultConfig(); - vaultConfig.setVaultId(""); // Vault ID - vaultConfig.setClusterId(""); // Cluster ID - vaultConfig.setEnv(Env.PROD); // Environment (e.g., DEV, PROD) - vaultConfig.setCredentials(vaultCredentials); // Setting credentials - - // Creating a Skyflow client instance with the configured vault + // Let the SDK generate bearer tokens from service account credentials + Credentials credentials = new Credentials(); + credentials.setCredentialsString(""); + + ConnectionConfig connectionConfig = new ConnectionConfig(); + connectionConfig.setConnectionId(""); + connectionConfig.setConnectionUrl(""); + connectionConfig.setCredentials(credentials); + Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.ERROR) // Setting log level to ERROR - .addVaultConfig(vaultConfig) // Adding vault configuration + .setLogLevel(LogLevel.ERROR) + .addConnectionConfig(connectionConfig) .build(); - // Attempting to detokenize data using the Skyflow client try { - detokenizeData(skyflowClient); + invokeConnection(skyflowClient); } catch (SkyflowException e) { - // Retry detokenization if the error is due to unauthorized access (HTTP 401) if (e.getHttpCode() == 401) { - detokenizeData(skyflowClient); + // The SDK generates a new bearer token at the start of this call + invokeConnection(skyflowClient); } else { - // Rethrow the exception for other error codes throw e; } } } catch (SkyflowException e) { - // Handling any exceptions that occur during the process System.out.println("An error occurred: " + e.getMessage()); } } - /** - * Method to detokenize data using the Skyflow client. - * It sends a detokenization request with a list of tokens and prints the response. - * - * @param skyflowClient The Skyflow client instance used for detokenization. - * @throws SkyflowException If an error occurs during the detokenization process. - */ - public static void detokenizeData(Skyflow skyflowClient) throws SkyflowException { - // Creating a list of tokens to be detokenized - ArrayList tokenList = new ArrayList<>(); - tokenList.add(""); // First token - tokenList.add(""); // Second token - - // Building a detokenization request with the token list and configuration - DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() - .tokens(tokenList) // Adding tokens to the request - .continueOnError(false) // Stop on error - .redactionType(RedactionType.PLAIN_TEXT) // Redaction type (e.g., PLAIN_TEXT) - .build(); + public static void invokeConnection(Skyflow skyflowClient) throws SkyflowException { + Map requestBody = new HashMap<>(); + requestBody.put("", ""); + + Map requestHeaders = new HashMap<>(); + requestHeaders.put("Content-Type", "application/json"); - // Sending the detokenization request and receiving the response - DetokenizeResponse detokenizeResponse = skyflowClient.vault().detokenize(detokenizeRequest); + InvokeConnectionRequest invokeConnectionRequest = InvokeConnectionRequest.builder() + .method(RequestMethod.POST) + .requestBody(requestBody) + .requestHeaders(requestHeaders) + .build(); - // Printing the detokenized response - System.out.println(detokenizeResponse); + InvokeConnectionResponse invokeConnectionResponse = + skyflowClient.connection("").invoke(invokeConnectionRequest); + System.out.println(invokeConnectionResponse); } } ``` diff --git a/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java index 48889218..09857a3c 100644 --- a/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java +++ b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java @@ -1,88 +1,70 @@ package com.example.serviceaccount; import com.skyflow.Skyflow; +import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; -import com.skyflow.config.VaultConfig; -import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; -import com.skyflow.enums.RedactionType; +import com.skyflow.enums.RequestMethod; import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.tokens.DetokenizeData; -import com.skyflow.vault.tokens.DetokenizeRequest; -import com.skyflow.vault.tokens.DetokenizeResponse; -import io.github.cdimascio.dotenv.Dotenv; -import java.util.ArrayList; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import com.skyflow.vault.connection.InvokeConnectionResponse; + +import java.util.HashMap; +import java.util.Map; /** - * This example demonstrates how to configure and use the Skyflow SDK - * to detokenize sensitive data stored in a Skyflow vault. - * It includes setting up credentials, configuring the vault, and - * making a detokenization request. The code also implements a retry - * mechanism to handle unauthorized access errors (HTTP 401). + * This example demonstrates how to handle a bearer token that expires while a + * connection request is in flight. Vault and Detect calls retry once on their own + * when this happens, but connection calls return the 401 error, so this example + * retries the connection call once when it fails with HTTP 401. */ public class BearerTokenExpiryExample { public static void main(String[] args) { try { - // Setting up credentials for accessing the Skyflow vault - Credentials vaultCredentials = new Credentials(); - vaultCredentials.setCredentialsString(""); + // Let the SDK generate bearer tokens from service account credentials + Credentials credentials = new Credentials(); + credentials.setCredentialsString(""); - // Configuring the Skyflow vault with necessary details - VaultConfig vaultConfig = new VaultConfig(); - vaultConfig.setVaultId(""); // Vault ID - vaultConfig.setClusterId(""); // Cluster ID - vaultConfig.setEnv(Env.PROD); // Environment (e.g., DEV, PROD) - vaultConfig.setCredentials(vaultCredentials); // Setting credentials + ConnectionConfig connectionConfig = new ConnectionConfig(); + connectionConfig.setConnectionId(""); + connectionConfig.setConnectionUrl(""); + connectionConfig.setCredentials(credentials); - // Creating a Skyflow client instance with the configured vault Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.ERROR) // Setting log level to ERROR - .addVaultConfig(vaultConfig) // Adding vault configuration + .setLogLevel(LogLevel.ERROR) + .addConnectionConfig(connectionConfig) .build(); - // Attempting to detokenize data using the Skyflow client try { - detokenizeData(skyflowClient); + invokeConnection(skyflowClient); } catch (SkyflowException e) { - // Retry detokenization if the error is due to unauthorized access (HTTP 401) if (e.getHttpCode() == 401) { - detokenizeData(skyflowClient); + // The SDK generates a new bearer token at the start of this call + invokeConnection(skyflowClient); } else { - // Rethrow the exception for other error codes throw e; } } } catch (SkyflowException e) { - // Handling any exceptions that occur during the process System.out.println("An error occurred: " + e.getMessage()); } } - /** - * Method to detokenize data using the Skyflow client. - * It sends a detokenization request with a list of tokens and prints the response. - * - * @param skyflowClient The Skyflow client instance used for detokenization. - * @throws SkyflowException If an error occurs during the detokenization process. - */ - public static void detokenizeData(Skyflow skyflowClient) throws SkyflowException { - // Creating a list of tokens to be detokenized - DetokenizeData detokenizeDataToken1 = new DetokenizeData("", RedactionType.PLAIN_TEXT); - DetokenizeData detokenizeDataToken2 = new DetokenizeData(""); - ArrayList detokenizeDataList = new ArrayList<>(); - detokenizeDataList.add(detokenizeDataToken1); // First token - detokenizeDataList.add(detokenizeDataToken2); // Second token + public static void invokeConnection(Skyflow skyflowClient) throws SkyflowException { + Map requestBody = new HashMap<>(); + requestBody.put("", ""); - // Building a detokenization request with the token list and configuration - DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() - .detokenizeData(detokenizeDataList) // Adding tokens to the request - .continueOnError(false) // Stop on error - .build(); + Map requestHeaders = new HashMap<>(); + requestHeaders.put("Content-Type", "application/json"); - // Sending the detokenization request and receiving the response - DetokenizeResponse detokenizeResponse = skyflowClient.vault().detokenize(detokenizeRequest); + InvokeConnectionRequest invokeConnectionRequest = InvokeConnectionRequest.builder() + .method(RequestMethod.POST) + .requestBody(requestBody) + .requestHeaders(requestHeaders) + .build(); - // Printing the detokenized response - System.out.println(detokenizeResponse); + InvokeConnectionResponse invokeConnectionResponse = + skyflowClient.connection("").invoke(invokeConnectionRequest); + System.out.println(invokeConnectionResponse); } } From 4c02b17e56ddab096de5a3327ba581a5f079fd79 Mon Sep 17 00:00:00 2001 From: "promptless[bot]" Date: Wed, 28 Oct 2026 05:28:16 +0000 Subject: [PATCH 2/2] SK-3253: Note behavior when token regeneration fails during a flowvault call --- flowvault/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flowvault/README.md b/flowvault/README.md index 62b588c5..1c4a646a 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -173,7 +173,7 @@ If none of the three yields credentials, the call fails with a `SkyflowException The SDK checks the bearer token once, at the start of each call, and a bulk call can then send many batch requests with that token. If the token expires partway through, Skyflow rejects the requests that follow with HTTP `401`. -The SDK recovers from this on its own when it generates tokens for you. That is the case with `setPath`, `setCredentialsString`, or the `SKYFLOW_CREDENTIALS` environment variable. If a request fails with `401` and its token expires within 60 seconds, the SDK generates a new token. It then sends that request once more. The remaining batches use the new token, so a bulk call that runs past the token's expiry still completes. Concurrent requests that fail on the same token share one new token. The SDK retries each request once and returns the response to that retry. It returns the original `401` instead when it can't generate a new token, or when the token wasn't close to expiry. +The SDK recovers from this on its own when it generates tokens for you. That is the case with `setPath`, `setCredentialsString`, or the `SKYFLOW_CREDENTIALS` environment variable. If a request fails with `401` and its token expires within 60 seconds, the SDK generates a new token. It then sends that request once more. The remaining batches use the new token, so a bulk call that runs past the token's expiry still completes. Concurrent requests that fail on the same token share one new token. The SDK retries each request once and returns the response to that retry. It returns the original `401` instead when it can't generate a new token, or when the token wasn't close to expiry. If generating a new token fails, the remaining requests in that call that use the expired token also return `401`, with no further attempt. Your next call generates a token again. This retry is always on and has no settings. It runs inside each transport attempt, so the overall `timeout` and the `maxRetries` policy in [Timeouts and retries](#timeouts-and-retries) still bound the whole call.