Skip to content
5 changes: 5 additions & 0 deletions .changeset/core-response-guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adobe/aio-commerce-lib-core": minor
---

Add response type guards to identify standardized SDK action, success, and error responses.
5 changes: 5 additions & 0 deletions .changeset/webhook-response-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adobe/aio-commerce-lib-webhooks": minor
---

Add webhook response helpers to identify webhook success responses and determine whether webhook action responses represent successful outcomes.
26 changes: 26 additions & 0 deletions packages/aio-commerce-lib-core/docs/guides/response-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,32 @@ if (response.type === "success") {

This allows TypeScript to narrow the type and provide accurate autocomplete and type checking.

## Checking Unknown Responses

Use `isActionResponse()`, `isSuccessResponse()`, or `isErrorResponse()` when you receive an unknown value and need to confirm that it matches the SDK action response shape before reading success or error fields:

```typescript
import {
isActionResponse,
isErrorResponse,
isSuccessResponse,
} from "@adobe/aio-commerce-lib-core/responses";

const result = await runAction(params);

if (isSuccessResponse(result)) {
console.log(`Status: ${result.statusCode}`);
}

if (isErrorResponse(result)) {
console.log(`Error Status: ${result.error.statusCode}`);
}

if (isActionResponse(result)) {
console.log(`Response type: ${result.type}`);
}
```

## Custom Status Codes

For custom status codes not covered by the presets, use the base functions directly:
Expand Down
17 changes: 16 additions & 1 deletion packages/aio-commerce-lib-core/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ throw new ApiError("Request failed", 500);
### Response Helpers

```typescript
import { ok, badRequest } from "@adobe/aio-commerce-lib-core/responses";
import {
ok,
badRequest,
isSuccessResponse,
isErrorResponse,
} from "@adobe/aio-commerce-lib-core/responses";

// Success response using string shorthand
return ok("User retrieved");
Expand All @@ -48,6 +53,16 @@ return badRequest("Invalid input");

// Or use full object syntax for additional data
return ok({ body: { message: "User retrieved", id: "123" } });

// Narrow unknown values before reading response fields
const result = await runAction(params);
if (isSuccessResponse(result)) {
console.log(result.statusCode);
}

if (isErrorResponse(result)) {
console.log(result.error.statusCode);
}
```

[Read the Response Helpers Guide →](./guides/response-helpers.md)
Expand Down
91 changes: 91 additions & 0 deletions packages/aio-commerce-lib-core/source/responses/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,97 @@ export type ActionResponse<
| SuccessResponse<TSuccessBody, THeaders>
| ErrorResponse<TErrorBody, THeaders>;

function isObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isHeadersRecord(value: unknown): value is HeadersRecord {
return (
value === undefined ||
(isObjectRecord(value) &&
Object.values(value).every((entry) => typeof entry === "string"))
);
}

function isBodyRecord(value: unknown): value is BodyRecord | undefined {
return value === undefined || isObjectRecord(value);
}

function isResponsePayload(value: unknown): value is ResponsePayload {
return (
isObjectRecord(value) &&
typeof value.statusCode === "number" &&
isBodyRecord(value.body) &&
isHeadersRecord(value.headers)
);
}

/**
* Determines whether a value is a standardized SDK success response.
*
* @param response - Value to inspect.
* @returns True when the value matches the SDK success response shape.
*
* @example
* ```typescript
* const result = await runAction(params);
* if (isSuccessResponse(result)) {
* console.log(result.statusCode);
* }
* ```
*/
export function isSuccessResponse(
response: unknown,
): response is SuccessResponse {
return (
isObjectRecord(response) &&
response.type === "success" &&
isResponsePayload(response)
);
}

/**
* Determines whether a value is a standardized SDK error response.
*
* @param response - Value to inspect.
* @returns True when the value matches the SDK error response shape.
*
* @example
* ```typescript
* const result = await runAction(params);
* if (isErrorResponse(result)) {
* console.log(result.error.statusCode);
* }
* ```
*/
export function isErrorResponse(response: unknown): response is ErrorResponse {
return (
isObjectRecord(response) &&
response.type === "error" &&
isResponsePayload(response.error)
);
}

/**
* Determines whether a value is a standardized SDK action response.
*
* @param response - Value to inspect.
* @returns True when the value matches the SDK action response shape.
*
* @example
* ```typescript
* const result = await runAction(params);
* if (isActionResponse(result) && result.type === "success") {
* console.log(result.statusCode);
* }
* ```
*/
export function isActionResponse(
response: unknown,
): response is ActionResponse {
return isSuccessResponse(response) || isErrorResponse(response);
}

/**
* Creates a standardized error response for runtime actions
* @see https://developer.adobe.com/app-builder/docs/guides/runtime_guides/creating-actions#unsuccessful-response
Expand Down
8 changes: 7 additions & 1 deletion packages/aio-commerce-lib-core/source/responses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
* @packageDocumentation
*/

export { buildErrorResponse, buildSuccessResponse } from "./helpers";
export {
buildErrorResponse,
buildSuccessResponse,
isActionResponse,
isErrorResponse,
isSuccessResponse,
} from "./helpers";
export * from "./presets";

export type { ActionResponse, ErrorResponse, SuccessResponse } from "./helpers";
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@

import { describe, expect, it } from "vitest";

import { buildErrorResponse, buildSuccessResponse } from "#responses/helpers";
import {
buildErrorResponse,
buildSuccessResponse,
isActionResponse,
isErrorResponse,
isSuccessResponse,
} from "#responses/helpers";

describe("responses/helpers", () => {
describe("buildErrorResponse", () => {
Expand Down Expand Up @@ -248,4 +254,84 @@ describe("responses/helpers", () => {
}
});
});

describe("isActionResponse", () => {
it("should return true for a success action response", () => {
const response = buildSuccessResponse(200, {
body: { message: "OK" },
headers: { "X-Request-Id": "abc123" },
});

expect(isActionResponse(response)).toBe(true);
expect(isSuccessResponse(response)).toBe(true);
expect(isErrorResponse(response)).toBe(false);
});

it("should return true for an error action response", () => {
const response = buildErrorResponse(400, {
body: { message: "Bad request" },
headers: { "X-Request-Id": "abc123" },
});

expect(isActionResponse(response)).toBe(true);
expect(isSuccessResponse(response)).toBe(false);
expect(isErrorResponse(response)).toBe(true);
});

it("should return false for non-object values", () => {
expect(isActionResponse(null)).toBe(false);
expect(isActionResponse(undefined)).toBe(false);
expect(isActionResponse("success")).toBe(false);
expect(isActionResponse([])).toBe(false);
expect(isSuccessResponse(null)).toBe(false);
expect(isErrorResponse(null)).toBe(false);
});

it("should return false when the response type is missing or unsupported", () => {
expect(isActionResponse({ statusCode: 200 })).toBe(false);
expect(isSuccessResponse({ statusCode: 200 })).toBe(false);
expect(isActionResponse({ statusCode: 200, type: "redirect" })).toBe(
false,
);
});

it("should return false for an invalid success response payload", () => {
expect(isActionResponse({ statusCode: "200", type: "success" })).toBe(
false,
);
expect(isSuccessResponse({ statusCode: "200", type: "success" })).toBe(
false,
);
expect(
isActionResponse({ body: null, statusCode: 200, type: "success" }),
).toBe(false);
expect(
isActionResponse({ body: [], statusCode: 200, type: "success" }),
).toBe(false);
expect(
isActionResponse({
headers: { "X-Request-Id": 123 },
statusCode: 200,
type: "success",
}),
).toBe(false);
});

it("should return false for an invalid error response payload", () => {
expect(isActionResponse({ type: "error" })).toBe(false);
expect(isErrorResponse({ type: "error" })).toBe(false);
expect(
isActionResponse({
error: { body: "Bad request", statusCode: 400 },
type: "error",
}),
).toBe(false);
expect(
isErrorResponse({
error: { body: "Bad request", statusCode: 400 },
type: "error",
}),
).toBe(false);
});
});
});
20 changes: 20 additions & 0 deletions packages/aio-commerce-lib-webhooks/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,26 @@ export async function customizeCheckout(params) {
}
```

### Checking Action Results

Because webhook actions can return HTTP 200 while still blocking the triggering process with an exception operation, callers can't rely on the status code alone to know whether a webhook handler actually succeeded. Use `isWebhookSuccessful()` to inspect an SDK response body instead:

```typescript
import {
isWebhookSuccessful,
isWebhookSuccessResponse,
} from "@adobe/aio-commerce-sdk/webhooks/responses";

const result = await handleWebhook(params);
if (!isWebhookSuccessful(result)) {
throw new Error("Webhook action failed");
}

if (isWebhookSuccessResponse(result)) {
console.log(result.body);
}
```

## Reference

For complete API documentation and additional details, see:
Expand Down
46 changes: 46 additions & 0 deletions packages/aio-commerce-lib-webhooks/source/responses/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { isWebhookSuccessResponse } from "./types";

/**
* Determines whether a webhook action's result represents a successful outcome.
* Adobe Commerce webhooks always respond with HTTP 200, even when the handler
* wants to block the triggering process, so the actual outcome is only visible
* in the response body's `op` field (`op: "exception"` signals a failure).
*
* @param result - The result of the instrumented webhook action.
* @returns True if the webhook response is successful, false otherwise.
*
* @example
* ```typescript
* import { isWebhookSuccessful } from "@adobe/aio-commerce-lib-webhooks/responses";
*
* const result = await runWebhookAction(params);
* span.setStatus(isWebhookSuccessful(result) ? { code: SpanStatusCode.OK } : { code: SpanStatusCode.ERROR });
* ```
*/
export function isWebhookSuccessful(result: unknown): boolean {
Comment thread
obarcelonap marked this conversation as resolved.
if (!isWebhookSuccessResponse(result)) {
return false;
}

if (!result.body) {
return true;
}
Comment thread
obarcelonap marked this conversation as resolved.

if (Array.isArray(result.body)) {
return result.body.every((operation) => operation.op !== "exception");
}

return result.body.op !== "exception";
}
4 changes: 4 additions & 0 deletions packages/aio-commerce-lib-webhooks/source/responses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@

/** biome-ignore-all lint/performance/noBarrelFile: This is the public API for the webhook responses entrypoint */

// Export response helpers
export * from "./helpers";
// Export all webhook operations
export * from "./operations/presets";
// Export HTTP response wrappers
export * from "./presets";
export { isWebhookSuccessResponse } from "./types";

// Export webhook operation types
export type {
Expand All @@ -26,3 +29,4 @@ export type {
SuccessOperation,
WebhookOperationResponse,
} from "./operations/types";
export type { WebhookSuccessResponse } from "./types";
Loading