> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tensor9.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Explaining SDK Requests

> Add a signed per-request Explain control, preserve the native response and retrieve its report.

Point the regular AWS SDK at Tensor9's synthetic AWS service endpoint using the application's normal IAM credentials and SigV4 signing. Add a per-request control when the request your application already constructs is the case you need to understand. The control must be added before signing and remain present on every retry.

The operation response stays in the AWS service's protocol and carries an explanation ID. Retrieve the explanation through the dedicated synthetic AWS `t9` service endpoint using SigV4 signing name `t9`. The AWS SDK does not parse the explanation as an operation result.

This guide uses AWS SDK for Java 2.x and S3 `PutObject`. The application keeps its S3 model and configured Cloud Adapter endpoint. The example selects `ExecuteAndExplain`, so it performs the upload.

## Check installed support

> [!NOTE]
>
> This guide describes the dedicated `t9` report protocol. Its retrieval examples require an adapter version that publishes `T9_ENDPOINT_URL` and `T9_SIGNING_REGION` for the install. This page does not establish report-service availability in an older deployment. If published settings are absent, check that deployment's version and documentation with Tensor9 rather than deriving a report URL from an origin-service endpoint.

## Decide whether the request may execute

| Control | Service action | SDK response behavior | Appropriate use |
| - | - | - | - |
| `{"version":1,"mode":"Explain","fmt":"Json"}` | Not performed | A completed preview returns AWS-framed `409 ExplainedNotPerformed` and a report receipt | Preview the request, then retrieve its report separately |
| `{"version":1,"mode":"ExecuteAndExplain","fmt":"Json"}` | Performed | Returns the ordinary service response and report receipt | An authorized test or real operation whose effects are intended |
| `true` | Performed | Same native-response behavior as `ExecuteAndExplain`, Human report | A concise opt-in when the executing semantics are already understood |

`ExecuteAndExplain` preserves the service's native status, headers and body. `Explain` suppresses the operation and returns an AWS service error rather than a fabricated successful `PutObjectResponse`. Validation and authorization refusals remain service errors. Neither mode returns an explanation body or wraps the native response in a diagnostic envelope.

> [!WARNING]
>
> The `true` shorthand performs the request. It is not equivalent to an explicit version-1 control whose mode is `Explain`.

## Configure the S3 client

Set Tensor9's synthetic AWS endpoint either process-wide or on this client, as described in the [AWS Java SDK guide](https://docs.tensor9.com/cloud-adapter-early-access/guides/aws-java-sdk.md). Keep the origin IAM credentials used for SigV4 separate from the target-cloud identity held by the adapter. The report client is configured separately with the deployment's `t9` endpoint and signing region, signing service `t9`, and the report owner's credentials.

For one client:

```java theme={null}
import java.net.URI;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

S3Client s3 = S3Client.builder()
    .endpointOverride(URI.create(System.getenv("T9_CLOUD_ADAPTER_ENDPOINT")))
    .region(Region.US_EAST_1)
    .credentialsProvider(credentials)
    .forcePathStyle(true)
    .build();
```

The caller supplies its normal configured `AwsCredentialsProvider`; do not place an access key in source code or diagnostic output.

## Execute one upload and request a report

`AwsRequestOverrideConfiguration` adds the header before the SDK signs this request:

```java theme={null}
import java.net.URI;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;

public final class ExplainUploadExample {
  private ExplainUploadExample() {}

  public static PutObjectResponse upload(
      URI endpoint,
      AwsCredentialsProvider credentials,
      String bucket,
      String key) {
    try (S3Client client = S3Client.builder()
        .endpointOverride(endpoint)
        .region(Region.US_EAST_1)
        .credentialsProvider(credentials)
        .forcePathStyle(true)
        .build()) {
      PutObjectRequest request = PutObjectRequest.builder()
          .bucket(bucket)
          .key(key)
          .overrideConfiguration(c -> c.putHeader(
              "x-t9-explain",
              "{\"version\":1,\"mode\":\"ExecuteAndExplain\",\"fmt\":\"Json\"}"))
          .build();

      PutObjectResponse response = client.putObject(
          request,
          RequestBody.fromString("hello"));

      System.out.println("HTTP " + response.sdkHttpResponse().statusCode());
      System.out.println("Report " + response.sdkHttpResponse()
          .firstMatchingHeader("x-t9-explain-id")
          .orElse("not supplied"));
      System.out.println("Mode " + response.sdkHttpResponse()
          .firstMatchingHeader("x-t9-explain-mode")
          .orElse("not supplied"));
      return response;
    }
  }
}
```

This is an executing example. Use a unique test key, verify the object in the target service, and remove it only after you have preserved the report and verified what was created. A `PutObjectResponse` or ETag establishes neither report completeness nor support for multipart or resumable upload.

## Capture receipts on success and service errors

Capture receipt headers as soon as an HTTP response arrives, before the SDK decodes its body. Preserve them on success, service errors and decoding failures. A request that fails before any HTTP response arrives has no receipt to capture. This complete class is compiled against the same AWS SDK major version used by the application example:

```java theme={null}
import java.net.URI;
import java.util.function.Consumer;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.core.interceptor.Context;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.http.SdkHttpResponse;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

public final class ExplainReceiptCaptureExample {
  private ExplainReceiptCaptureExample() {}

  public record ExplainReceipt(
      String reportId,
      String mode,
      String unexplained,
      String expiresAt) {
    static ExplainReceipt from(SdkHttpResponse response) {
      return new ExplainReceipt(
          response.firstMatchingHeader("x-t9-explain-id").orElse(null),
          response.firstMatchingHeader("x-t9-explain-mode").orElse(null),
          response.firstMatchingHeader("x-t9-explain-unexplained").orElse(null),
          response.firstMatchingHeader("x-t9-explain-expires-at").orElse(null));
    }
  }

  public static ExecutionInterceptor capture(Consumer<ExplainReceipt> saveReceipt) {
    return new ExecutionInterceptor() {
      @Override
      public void afterTransmission(
          Context.AfterTransmission context,
          ExecutionAttributes attributes) {
        ExplainReceipt receipt = ExplainReceipt.from(context.httpResponse());
        if (receipt.reportId() == null && receipt.unexplained() == null) return;
        try {
          // Save every attempt's receipt. Retrieve report content separately.
          saveReceipt.accept(receipt);
        } catch (RuntimeException failure) {
          // A diagnostic storage failure must not change the service operation's result.
          System.err.println("Unable to store explanation receipt");
        }
      }
    };
  }

  public static S3Client client(
      URI endpoint,
      AwsCredentialsProvider credentials,
      Consumer<ExplainReceipt> saveReceipt) {
    return S3Client.builder()
        .endpointOverride(endpoint)
        .region(Region.US_EAST_1)
        .credentialsProvider(credentials)
        .forcePathStyle(true)
        .overrideConfiguration(c -> c.addExecutionInterceptor(capture(saveReceipt)))
        .build();
  }
}
```

Construct the client with `ExplainReceiptCaptureExample.client` and a thread-safe receipt sink that records or queues each attempt promptly. Keep the signed per-request control from the upload example. Retrieve report content outside the interceptor so report polling does not delay the service call.

A service error can still carry a report receipt. A transport error might have no HTTP response at all. Neither case is permission to replay a write. First inspect target state and the operation's idempotency.

## Handle preview mode deliberately

To preview the same request, change only the control value:

```java theme={null}
.overrideConfiguration(c -> c.putHeader(
    "x-t9-explain",
    "{\"version\":1,\"mode\":\"Explain\",\"fmt\":\"Json\"}"))
```

The adapter does not perform `PutObject`. When the preview completes, the SDK receives an S3-shaped `409 ExplainedNotPerformed` service error carrying `x-t9-explain-id` and `x-t9-explain-expires-at`. Handle it as a completed preview, retain its receipt and retrieve the report separately. Do not retry it as a transient failure or convert it to a successful upload result. Other service errors can describe a refused preview; inspect their native error code and the report when one is supplied.

The response remains in the S3 protocol even if the request includes `Accept: application/vnd.tensor9.explain+json`. That media type does not request an inline explanation. Use [`tensor9 explain`](https://docs.tensor9.com/cloud-adapter-early-access/debugging/using-explain) when the directed profile, rather than a deployed request, is the question.

## Keep the control signed and singular

Request override configuration is applied before SigV4 signing. Middleware that mutates the final HTTP request afterward can invalidate the signature or leave the Explain control unsigned.

Inspect the final request construction if the request is refused:

* add exactly one `x-t9-explain` field;
* do not combine a default interceptor and a request override that append two fields;
* use the exact case-sensitive JSON enum values;
* keep the header ASCII and under 4096 bytes;
* ensure the signing identity covers `x-t9-explain`.

If the SDK retries an executing request, each execution attempt can have its own report. Preserve each report ID rather than overwriting the first with the last. A preview response is a service error, so it never needs diagnostic-body reconstruction.

## Configure the t9 report client

Keep the application-service client and report client configured separately:

| Client | Endpoint configuration | SigV4 signing service | Region and credentials |
| - | - | - | - |
| S3 application client | Its configured S3 adapter endpoint | `s3` | Application's origin region and IAM provider |
| Explanation client | `T9_ENDPOINT_URL` | `t9` | `T9_SIGNING_REGION` and the exact report owner's IAM provider, including any session token |

For a local `tensor9 adapt svc run` process, copy its `t9 endpoint` and `t9 signing region` readiness values (`t9Endpoint` and `t9SigningRegion` in JSON output). They are report-client settings, not a replacement for the S3 client endpoint.

For an existing provisioned fabric accessed through `tensor9 adapt cloud connect`, use the directory for that fabric:

```sh theme={null}
tensor9 adapt cloud connect -dir "$FABRIC_DIR" -env
```

Its environment output includes `T9_ENDPOINT_URL` and `T9_SIGNING_REGION` alongside the AWS connection settings. Keep the forwarding session running and use the output for the same deployment that produced the report. Do not derive a report endpoint from an S3 URL, report ID or vanity-domain text.

A local forwarded URI can be shared physically by several services. Continue configuring the report client explicitly and signing as `t9`; sharing a URI does not make the report API part of S3.

```java theme={null}
URI t9Endpoint = URI.create(System.getenv("T9_ENDPOINT_URL"));
Region t9SigningRegion = Region.of(System.getenv("T9_SIGNING_REGION"));
```

## Retrieve without replaying

Once you have `x-t9-explain-id`, call the dedicated synthetic AWS service `t9`. Configure its endpoint and signing region explicitly; do not reuse the S3 endpoint or S3 signing name. The report API uses AWS JSON 1.1:

```http theme={null}
POST / HTTP/1.1
Host: t9.example.test
Content-Type: application/x-amz-json-1.1
X-Amz-Target: T9CloudAdapter.GetExplanation
Authorization: AWS4-HMAC-SHA256 ...

{"reportId":"report-123","fmt":"Json"}
```

Sign the POST and its single `X-Amz-Target` header with regular IAM credentials, signing name **`t9`**, the report service's configured region, and the exact authenticated principal that owns the report. An explanation ID identifies a report; it grants no access by itself. A different role session is not the same owner.

The following complete class calls `GetExplanation` with the configured `t9` endpoint, report signing region and application's credentials provider, including any session token. The returned HTTP body is the outer AWS JSON object, so parse it and then read its `body` string:

```java theme={null}
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.HexFormat;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.http.ContentStreamProvider;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
import software.amazon.awssdk.http.auth.spi.signer.SignRequest;
import software.amazon.awssdk.regions.Region;

public final class ExplainReportRetrievalExample {
  private ExplainReportRetrievalExample() {}

  public static HttpResponse<String> read(
      URI t9Endpoint,
      AwsCredentialsProvider credentials,
      Region t9SigningRegion,
      String reportId) throws IOException, InterruptedException {
    // 1. Encode the report locator as JSON for the dedicated t9 service.
    if (reportId.isEmpty() || reportId.length() > 256
        || !reportId.chars().allMatch(c -> c >= 0x21 && c <= 0x7e)) {
      throw new IllegalArgumentException("Expected the report ID from x-t9-explain-id");
    }
    String escapedId = reportId.replace("\\", "\\\\").replace("\"", "\\\"");
    byte[] body = ("{\"reportId\":\"" + escapedId + "\",\"fmt\":\"Json\"}")
        .getBytes(StandardCharsets.UTF_8);
    // 2. Sign the operation target and exact payload with the report owner's IAM credentials.
    SdkHttpRequest unsigned = SdkHttpRequest.builder()
        .method(SdkHttpMethod.POST)
        .uri(t9Endpoint.resolve("/"))
        .putHeader("Content-Type", "application/x-amz-json-1.1")
        .putHeader("X-Amz-Target", "T9CloudAdapter.GetExplanation")
        .putHeader("x-amz-content-sha256", sha256(body))
        .build();
    SdkHttpRequest signed = AwsV4HttpSigner.create().sign(
        SignRequest.builder(credentials.resolveCredentials())
            .request(unsigned)
            .payload(ContentStreamProvider.fromByteArray(body))
            .putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, "t9")
            .putProperty(AwsV4HttpSigner.REGION_NAME, t9SigningRegion.id())
            .build()).request();

    // 3. Send the separate request without changing the original AWS operation's result.
    HttpRequest.Builder request = HttpRequest.newBuilder(signed.getUri())
        .timeout(Duration.ofSeconds(30))
        .POST(HttpRequest.BodyPublishers.ofByteArray(body));
    signed.headers().forEach((name, values) -> {
      // the Java client supplies Host from the exact URI that was signed.
      if (!name.equalsIgnoreCase("host")) {
        values.forEach(value -> request.header(name, value));
      }
    });
    return HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build()
        .send(request.build(), HttpResponse.BodyHandlers.ofString());
  }

  private static String sha256(byte[] body) {
    try {
      return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(body));
    } catch (NoSuchAlgorithmException impossible) {
      throw new IllegalStateException("The JDK must provide SHA-256", impossible);
    }
  }
}
```

The successful AWS JSON response contains `reportId`, `revision`, `fmt` and a **string** `body` containing the rendered report; `acknowledgeRevision` is optional. Decode this outer object before reading or parsing the report string. Responses use `Cache-Control: no-store`. `GetExplanation` never repeats `PutObject` or changes its outcome. For asynchronous work, repeat `GetExplanation` with bounded backoff and preserve monotonic revisions. Query resource status separately through the normal AWS SDK operation using the exact resource identity. That request still follows the adapter's configured default unless it supplies an explicit capture control; a report ID does not bypass the default.

The legacy `x-t9-explain-id` request selector on service operations is refused with `400 InvalidExplainConfiguration`. Call `GetExplanation` on `t9` instead of asking a status operation to return an explanation.

See [Diagnostic Response Headers](https://docs.tensor9.com/cloud-adapter-early-access/debugging/response-headers.md) for report reads and acknowledgment, and [Following Asynchronous Explanations](https://docs.tensor9.com/cloud-adapter-early-access/debugging/async-explanations.md) for partial revisions and lost final responses.

Select `fmt` as `Human`, `Agent` or `Json`. The AWS JSON response contains the rendered report in its `body` string; for `Json`, parse that string as JSON after decoding the outer response.

An optional `minimumVersion` requests a known revision or newer and must be a positive 64-bit integer. Omit it for a first read, including a collecting report at revision zero. `NotYetVisible`, `NotFound`, `Expired`, `Consumed`, `Unavailable` and other error dispositions do not replay the operation. Use bounded backoff for retryable reads and preserve earlier snapshots. Reading never acknowledges a sealed report; export needed evidence before the designated owner acknowledges it separately.

## PostgreSQL plans in the report

The [PostgreSQL query-plan guide](https://docs.tensor9.com/cloud-adapter-early-access/debugging/postgres-query-plans.md) describes the planned full-JSON integration for supported PostgreSQL-backed statements. It uses the same report ID and signed `t9` retrieval flow; no query plan replaces the SDK response. Planning-only EXPLAIN must not run the native write again. The guide explains the committed-state estimate, exact-owner access, sensitive predicates and explicit unavailable or omitted planning evidence. Support depends on the installed release and exact operation/backend; it is not implied by a PostgreSQL backend alone.

## Retain evidence before expiry

Reports expire **24 hours from creation** by default. The `x-t9-explain-expires-at` receipt gives the UTC expiry timestamp in RFC 3339 form; JSON reports include the same deadline in `reportExpiresAt`. Reading a report does not extend it. Export needed evidence before expiry or owner acknowledgment.

The adapter can configure a different duration with `T9_SIPSVR_EXPLAIN_RETENTION_SECS`; it applies to newly created reports. At expiry, capture stops and retrieval becomes unavailable without cancelling the underlying service operation. Physical collection of expired records is a separate, opt-in setting. See the [retention reference](https://docs.tensor9.com/cloud-adapter-early-access/debugging/response-headers.md#retention-and-expiry).

## Assign acknowledgment ownership

The authenticated principal recorded as the report owner is the principal that can read and acknowledge it through the `t9` report API. Acknowledging its sealed revision consumes that report. Decide responsibility before the request:

* the application can export a sanitized bundle and leave acknowledgment to the component acting as the report owner;
* an automated test that owns the report can acknowledge after its assertions and evidence export;
* people or systems that do not hold the owner identity receive only the sanitized evidence shared through an approved channel, not direct report-route access.

Never acknowledge merely because the native operation succeeded. First record the final report revision, declared limitations and evidence needed by other people. Call `T9CloudAdapter.AcknowledgeExplanation` through `t9` with `reportId` and the supplied `acknowledgeRevision` as `revision`. If the owner loses the response, repeating the same sealed revision is idempotent and returns `Consumed`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.