This commit is contained in:
JiriUhlir
2026-07-13 10:04:31 +02:00
parent e9d5f6bd36
commit a8279a8d5c
4 changed files with 93 additions and 37 deletions
+17 -6
View File
@@ -2,7 +2,7 @@ import "../client/axiosTypes";
import { AxiosInstance } from "axios";
import { z } from "zod";
import { SapB1Config } from "../config";
import { SapB1Error } from "../errors/SapB1Error";
import { SapB1Error, toSapB1Error } from "../errors/SapB1Error";
import { SapB1Logger, noopLogger, sanitizeLogMeta } from "../utils/logger";
const LoginResponseSchema = z.object({
@@ -78,11 +78,22 @@ export class SessionManager {
return this.session;
} catch (error) {
if (error instanceof SapB1Error) {
throw error;
}
throw new SapB1Error({ message: "SAP B1 login failed", cause: error });
// Route login failures through the shared normalizer so the SAP status/code and
// rejection message survive into the response. That is what lets the caller tell a
// SAP-side rejection (wrong CompanyDB/user/password -> HTTP 401/400 with a SAP body)
// apart from a transport problem (wrong BaseUrl/port, TLS, timeout -> no HTTP status).
const sapError = toSapB1Error(error, "POST", "/Login", "SAP B1 login failed");
this.logger.error(
"SAP B1 login failed",
sanitizeLogMeta({
baseUrl: this.config.baseUrl,
companyDB: this.config.companyDB,
status: sapError.status,
code: sapError.code,
message: sapError.message
})
);
throw sapError;
}
}
+3 -31
View File
@@ -1,10 +1,10 @@
import "./axiosTypes";
import https from "node:https";
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, Method } from "axios";
import axios, { AxiosInstance, AxiosRequestConfig, Method } from "axios";
import { z } from "zod";
import { SessionManager } from "../auth/SessionManager";
import { SapB1Config, SapB1ConfigSchema } from "../config";
import { SapB1Error } from "../errors/SapB1Error";
import { SapB1Error, toSapB1Error } from "../errors/SapB1Error";
import { ODataQuery, ODataResponse } from "../types/odata";
import { buildODataParams, extractNextLink } from "./odata";
import { SapB1Logger, noopLogger, sanitizeLogMeta } from "../utils/logger";
@@ -107,7 +107,7 @@ export class SapB1Client {
const response = await this.http.request(requestConfig);
return options.schema ? options.schema.parse(response.data) : (response.data as T);
} catch (error) {
const sapError = this.toSapB1Error(error, method, path);
const sapError = toSapB1Error(error, method, path);
const canRelogin = sapError.status === 401 && !requestConfig.skipAuth && !reloginAttempted;
if (canRelogin) {
@@ -132,34 +132,6 @@ export class SapB1Client {
throw new SapB1Error({ message: "SAP B1 request failed after retries", method, url: path });
}
private toSapB1Error(error: unknown, method: Method, path: string): SapB1Error {
if (error instanceof SapB1Error) {
return error;
}
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{ error?: { code?: string; message?: { value?: string } | string } }>;
const status = axiosError.response?.status;
const sapError = axiosError.response?.data?.error;
const message =
typeof sapError?.message === "string"
? sapError.message
: sapError?.message?.value || axiosError.message || "SAP B1 request failed";
return new SapB1Error({
status,
code: sapError?.code,
message,
method,
url: path,
retryable: status === 408 || status === 429 || (status !== undefined && status >= 500),
cause: error
});
}
return new SapB1Error({ message: "SAP B1 request failed", method, url: path, cause: error });
}
private buildServiceLayerBaseUrl(baseUrl: string): string {
const trimmed = baseUrl.replace(/\/+$/, "");
return trimmed.endsWith("/b1s/v1") ? trimmed : `${trimmed}/b1s/v1`;
+39
View File
@@ -1,3 +1,5 @@
import axios, { AxiosError, Method } from "axios";
export interface SapB1ErrorDetails {
status?: number;
code?: string;
@@ -27,3 +29,40 @@ export class SapB1Error extends Error {
this.cause = details.cause;
}
}
// Shared normalizer so every SAP call (including login) surfaces the SAP status/code
// and message when the Service Layer actively rejected the request, and falls back to
// the transport-level message (ECONNREFUSED, timeout, TLS, ...) otherwise. This is what
// lets a caller tell "SAP rejected it" apart from "we never reached SAP".
export function toSapB1Error(
error: unknown,
method?: Method,
path?: string,
fallbackMessage = "SAP B1 request failed"
): SapB1Error {
if (error instanceof SapB1Error) {
return error;
}
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{ error?: { code?: string; message?: { value?: string } | string } }>;
const status = axiosError.response?.status;
const sapError = axiosError.response?.data?.error;
const message =
typeof sapError?.message === "string"
? sapError.message
: sapError?.message?.value || axiosError.message || fallbackMessage;
return new SapB1Error({
status,
code: sapError?.code,
message,
method,
url: path,
retryable: status === 408 || status === 429 || (status !== undefined && status >= 500),
cause: error
});
}
return new SapB1Error({ message: fallbackMessage, method, url: path, cause: error });
}