Skip to content

Conversation

@narekhovhannisyan
Copy link
Collaborator

@narekhovhannisyan narekhovhannisyan commented Nov 11, 2025

Motivation

The axios logger was failing to properly extract error messages from API responses, resulting in unhelpful error messages like [object Object] instead of meaningful root cause information. This made debugging difficult for developers using the SDK, as they couldn't identify what went wrong with their API requests.

The logger only handled basic error formats (single error strings, arrays of strings, and objects with name/base properties) but failed to handle:

  • Arrays of error objects with nested field-specific errors
  • Objects with direct field-specific errors (e.g., { "email": ["is invalid"] })
  • Arrays of objects with direct base field (no identifier)

Additionally, the code used for...of loops which violated linting rules requiring array methods instead.

Changes

  • Enhanced error message extraction - Now properly handles all error response formats:

    • Arrays of error objects with nested errors (e.g., contact imports validation)
    • Objects with field-specific errors (e.g., { "email": ["is invalid"] })
    • Arrays of objects with direct base field
    • Arrays of strings
    • Single error strings
    • Objects with name/base properties (legacy format)
  • Refactored to use array methods - Replaced all for...of loops with functional array methods (map, filter, reduce) to comply with linting rules

  • Extracted simple, focused functions - Broke down complex logic into smaller, testable functions:

    • formatFieldError - Formats a single field error message
    • extractFieldErrors - Extracts all field errors from an object
    • getErrorIdentifier - Gets email/id identifier from error object
    • extractNestedErrors - Extracts errors from nested "errors" property
    • extractDirectErrors - Extracts errors directly from object properties
    • formatErrorMessage - Formats a single error object into message string
  • Updated tests - Fixed test expectations to match the improved error message extraction behavior

How to test

  • Run existing test suite: npm test -- src/__tests__/lib/axios-logger.test.ts src/__tests__/lib/api/resources/ContactImports.test.ts src/__tests__/lib/mailtrap-client.test.ts

  • Test with real API calls that trigger various error formats:

    • Contact imports with invalid emails (array of error objects)
    • Contact creation with invalid email (object with field errors)
    • Contact list creation with empty name (object with name/base errors)
    • Sending API with missing fields (array of strings)
    • Non-existent resource requests (single error string)
  • Verify error messages are human-readable and show root cause (no more [object Object])

  • Verify linting passes: npm run lint

  • Verify build succeeds: npm run build

Summary by CodeRabbit

  • Bug Fixes

    • Improved API error-message formatting — arrays, nested field errors and identifiers are now consolidated into clearer, user-facing messages.
  • Tests

    • Added and updated extensive unit tests to validate error-message extraction/formatting across many API error shapes and edge cases, ensuring consistent, readable error text.

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Warning

Rate limit exceeded

@narekhovhannisyan has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 8 minutes and 30 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 8f35cc8 and d54fe04.

📒 Files selected for processing (1)
  • src/lib/axios-logger.ts (1 hunks)

Walkthrough

Centralizes and refactors Axios error handling into helper functions that extract and format error messages from varied response shapes, updates handleSendingError to use the new builder, and adjusts/extends unit tests to validate the new message formats and edge cases.

Changes

Cohort / File(s) Change Summary
Axios error formatting refactor
src/lib/axios-logger.ts
Introduces helper functions (e.g., formatFieldError, extractFieldErrors, getErrorIdentifier, extractNestedErrors, extractDirectErrors, formatErrorMessage, extractMessagesFromErrorObjects, extractErrorMessage, buildErrorMessage) and centralizes error-message construction; handleSendingError now delegates to buildErrorMessage and throws MailtrapError with the composed message and original error as cause.
Unit tests for axios-logger
src/__tests__/lib/axios-logger.test.ts
Adds comprehensive tests covering single-string errors, arrays of strings, arrays of error objects, nested errors, field maps, identifier-only errors, plain-text responses, fallbacks for null/invalid response data, and preservation of the original Axios error as cause.
Test expectation updates
src/__tests__/lib/api/resources/ContactImports.test.ts, src/__tests__/lib/api/resources/ContactExports.test.ts
Updates expected error messages to match new composed formatting (e.g., formatted messages for arrays of validation objects) and aligns tests to fallback behavior for unrecognized shapes (e.g., "Request failed with status code 422"). Comments adjusted/removed accordingly.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Axios as AxiosRequest
    participant Logger as handleSendingError
    participant Builder as buildErrorMessage
    participant Extractor as extractErrorMessage
    participant Formatter as formatErrorMessage
    participant MailtrapError as MailtrapError

    Client->>Axios: perform request
    Axios-->>Client: throws AxiosError (response.data)
    Client->>Logger: handleSendingError(error)
    Logger->>Builder: buildErrorMessage(error)
    Builder->>Extractor: extractErrorMessage(response.data, default)
    note right of Extractor `#f0f4ff`: Inspect `data.error` / `data.errors` / nested shapes
    alt errors is array of objects
        Extractor->>Formatter: extractMessagesFromErrorObjects(...)
        Formatter-->>Extractor: concatenated message
    else errors is field map
        Extractor->>Formatter: extractFieldErrors(...)
        Formatter-->>Extractor: joined field messages
    else plain string / unknown
        Extractor-->>Builder: return default or `error.message`
    end
    Extractor-->>Builder: composed message
    Builder-->>Logger: final message
    Logger->>MailtrapError: throw MailtrapError(message) with cause = original error
    MailtrapError-->>Client: propagated MailtrapError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Files/areas to review carefully:
    • src/lib/axios-logger.ts — ordering and correctness of multi-shape extraction/formatting logic.
    • src/__tests__/lib/axios-logger.test.ts — coverage, expected message strings, and cause preservation.
    • Updated expectations in ContactImports.test.ts and ContactExports.test.ts — ensure alignment with other callers of handleSendingError.

Possibly related PRs

  • Contact imports #95 — Related to axios-logger error extraction changes impacting ContactImports tests.
  • Contact exports  #98 — Related changes affecting error-message formatting used by ContactExports tests.

Suggested reviewers

  • VladimirTaytor
  • mklocek

Poem

🐰 I hopped through messy JSON and strings,

I stitched field errors into tidy things,
IDs and messages now sing in a row,
Tests nod when the composed messages show,
A carrot of clarity — hop, fix, and go! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix axios error parsing' clearly and concisely summarizes the main change—improving axios error message extraction from API responses.
Description check ✅ Passed The description includes all key sections: Motivation (explains the problem and impact), Changes (lists all enhancements with details), and How to test (provides specific test commands and scenarios).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@narekhovhannisyan narekhovhannisyan self-assigned this Nov 11, 2025
@narekhovhannisyan narekhovhannisyan linked an issue Nov 11, 2025 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/axios-logger.ts (1)

65-76: Capture scalar direct errors before they fall back to [object Object].

Objects like { errors: [{ email: "[email protected]", message: "already taken" }] } still degrade because non-array properties are discarded in extractDirectErrors, forcing the final fallback to [object Object]. Wrapping simple scalars (string/number/boolean) into arrays preserves those messages and keeps them human-readable.

Apply this diff so scalars survive the pipeline:

 function extractDirectErrors(errorObj: Record<string, unknown>): string[] {
   const directErrors = Object.entries(errorObj)
     .filter(
       ([field]) => field !== "email" && field !== "id" && field !== "errors"
     )
     .reduce((acc, [field, value]) => {
-      acc[field] = value;
+      if (Array.isArray(value)) {
+        acc[field] = value;
+      } else if (
+        typeof value === "string" ||
+        typeof value === "number" ||
+        typeof value === "boolean"
+      ) {
+        acc[field] = [value];
+      }
       return acc;
-    }, {} as Record<string, unknown>);
+    }, {} as Record<string, unknown>);
 
   return extractFieldErrors(directErrors);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa2b59 and ae6bfda.

📒 Files selected for processing (2)
  • src/__tests__/lib/api/resources/ContactImports.test.ts (1 hunks)
  • src/lib/axios-logger.ts (1 hunks)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/__tests__/lib/axios-logger.test.ts (2)

360-527: Duplicate test cases for null response data.

The tests "falls back to default message when response data is invalid" (lines 360-390) and "returns default message when data is null" (lines 497-527) are duplicates. Both test the same scenario where responseData = null and verify the error message falls back to the default.

Remove one of the duplicate tests:

-    it("returns default message when data is null", () => {
-      const responseData = null;
-      // @ts-ignore
-      const axiosError = new AxiosError(
-        "Network error",
-        "500",
-        { headers: {} } as any,
-        {
-          data: responseData,
-          status: 500,
-        }
-      );
-      axiosError.response = {
-        data: responseData,
-        status: 500,
-        statusText: "Internal Server Error",
-        headers: {},
-        config: {} as any,
-      };
-
-      expect.assertions(2);
-
-      try {
-        axiosLogger(axiosError);
-      } catch (error) {
-        expect(error).toBeInstanceOf(MailtrapError);
-        if (error instanceof MailtrapError) {
-          expect(error.message).toBe("Network error");
-        }
-      }
-    });
-

53-673: Consider extracting test helper to reduce boilerplate.

The test cases follow a repetitive pattern with significant code duplication: creating AxiosError, setting the response, wrapping in try/catch, and asserting error type/message. A helper function could improve maintainability and readability.

Example helper function to add at the beginning of the describe block:

function createAxiosErrorAndExpect(
  message: string,
  code: string,
  status: number,
  statusText: string,
  responseData: any,
  expectedMessage: string,
  additionalAssertions?: (error: MailtrapError) => void
) {
  const axiosError = new AxiosError(
    message,
    code,
    { headers: {} } as any,
    { data: responseData, status }
  );
  axiosError.response = {
    data: responseData,
    status,
    statusText,
    headers: {},
    config: {} as any,
  };

  try {
    axiosLogger(axiosError);
    fail("Expected axiosLogger to throw");
  } catch (error) {
    expect(error).toBeInstanceOf(MailtrapError);
    if (error instanceof MailtrapError) {
      expect(error.message).toBe(expectedMessage);
      additionalAssertions?.(error);
    }
  }
}

Then tests can be simplified to:

it("extracts single error string from response data", () => {
  const responseData = { error: "Not Found" };
  createAxiosErrorAndExpect(
    "Request failed",
    "404",
    404,
    "Not Found",
    responseData,
    "Not Found"
  );
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3a005c and 56fd465.

📒 Files selected for processing (1)
  • src/__tests__/lib/axios-logger.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/__tests__/lib/axios-logger.test.ts (1)
src/lib/MailtrapError.ts (1)
  • MailtrapError (1-1)
🔇 Additional comments (3)
src/__tests__/lib/axios-logger.test.ts (3)

1-51: LGTM! Baseline error handling tests are well-structured.

The initial tests appropriately verify that non-axios errors are re-thrown unchanged and axios errors are wrapped in MailtrapError. The test structure is clear and the assertions are appropriate.


53-359: Excellent coverage of error extraction scenarios.

These tests comprehensively validate error message extraction for the various formats mentioned in the PR objectives:

  • Single error strings and arrays of strings
  • Nested error objects with field-specific errors
  • Direct base field errors
  • Legacy name/base formats
  • Multiple field errors with proper concatenation

The expected outputs (e.g., line 168: "invalid-email-1: email: is invalid, is required | invalid-email-2: Contact limit exceeded") demonstrate proper formatting without [object Object] issues.


392-424: LGTM! Cause property preservation is properly tested.

This test appropriately verifies that the original AxiosError is preserved in the cause property of the thrown MailtrapError, maintaining error context for debugging. The // @ts-expect-error comment on line 419 is appropriate given ES5 type limitations.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56fd465 and 2c0bf84.

📒 Files selected for processing (3)
  • src/__tests__/lib/api/resources/ContactExports.test.ts (1 hunks)
  • src/__tests__/lib/axios-logger.test.ts (1 hunks)
  • src/lib/axios-logger.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/__tests__/lib/axios-logger.test.ts (1)
src/lib/MailtrapError.ts (1)
  • MailtrapError (1-1)
🪛 GitHub Actions: test
src/lib/axios-logger.ts

[error] 132-132: TS2638: Type '{}' may represent a primitive value, which is not permitted as the right operand of the 'in' operator.

🪛 GitHub Check: lint
src/lib/axios-logger.ts

[failure] 137-137:
Type '{}' may represent a primitive value, which is not permitted as the right operand of the 'in' operator.


[failure] 132-132:
Type '{}' may represent a primitive value, which is not permitted as the right operand of the 'in' operator.

Comment on lines 131 to 187
// error is in `data.error`
if ("error" in data && data.error) {
return String(data.error);
}

// errors are in `data.errors`
if ("errors" in data && data.errors) {
const { errors } = data;

// errors is an array of strings
if (Array.isArray(errors) && errors.length > 0) {
if (typeof errors[0] === "string") {
return errors.join(",");
}

// errors is an array of objects
if (typeof errors[0] === "object" && errors[0] !== null) {
const extracted = extractMessagesFromErrorObjects(
errors as Array<Record<string, unknown>>
);
if (extracted) {
return extracted;
}
}
}

// errors is an object (could have name/base or field-specific errors)
if (
typeof errors === "object" &&
!Array.isArray(errors) &&
errors !== null
) {
// check for name/base properties first (legacy format)
const errorNames =
hasErrorProperty(errors, "name") && errors.name.join(", ");
const errorBase =
hasErrorProperty(errors, "base") && errors.base.join(", ");

if (errorNames) return errorNames;
if (errorBase) return errorBase;

// extract field-specific errors (e.g., { "email": ["is invalid", ...] })
const fieldMessages = extractFieldErrors(
errors as Record<string, unknown>
);

if (fieldMessages.length > 0) {
return fieldMessages.join("; ");
}

// If errors object doesn't match any recognized pattern, fall back to default message
return defaultMessage;
}

// If errors doesn't match any recognized format, fall back to default message
return defaultMessage;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the object guard before using "in" on data.
data is still typed as unknown when we hit the "error" in data / "errors" in data checks, so TypeScript rightfully raises TS2638 and the build fails. Please narrow data to a non-null object (e.g., via if (typeof data !== "object" || data === null)) before running these property checks, or assign it to a Record<string, unknown> helper variable.

Apply this diff to resolve the type error and keep the runtime behavior intact:

-  // error is in `data.error`
-  if ("error" in data && data.error) {
-    return String(data.error);
+  const dataRecord = data as Record<string, unknown>;
+
+  // error is in `data.error`
+  if ("error" in dataRecord && dataRecord.error) {
+    return String(dataRecord.error);
   }
 
   // errors are in `data.errors`
-  if ("errors" in data && data.errors) {
-    const { errors } = data;
+  if ("errors" in dataRecord && dataRecord.errors) {
+    const { errors } = dataRecord;

(Be sure to adjust the subsequent uses of errors to reference dataRecord.) This satisfies the compiler and prevents the CI failure.

🧰 Tools
🪛 GitHub Actions: test

[error] 132-132: TS2638: Type '{}' may represent a primitive value, which is not permitted as the right operand of the 'in' operator.

🪛 GitHub Check: lint

[failure] 137-137:
Type '{}' may represent a primitive value, which is not permitted as the right operand of the 'in' operator.


[failure] 132-132:
Type '{}' may represent a primitive value, which is not permitted as the right operand of the 'in' operator.

🤖 Prompt for AI Agents
In src/lib/axios-logger.ts around lines 131 to 187, the code uses `"error" in
data` and `"errors" in data` while data is still typed as unknown, causing
TS2638; narrow data to a non-null object first (e.g., if (typeof data !==
"object" || data === null) return defaultMessage; or const dataRecord = data as
Record<string, unknown> after that guard) and then use dataRecord for all
subsequent property checks and accesses (including replacing uses of errors with
dataRecord.errors) so the TypeScript compiler is satisfied and runtime behavior
remains unchanged.

@narekhovhannisyan narekhovhannisyan merged commit 6bd88de into main Nov 16, 2025
4 checks passed
@narekhovhannisyan narekhovhannisyan deleted the fix-axios-error-parsing branch November 16, 2025 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix axios logger/error handler

4 participants