fix(paypal): log no request bodies, headers or PayPal orders

The PayPal SDK client logged every request body to the console, and
with it the buyer's name and address in the order request, as well as
the response headers. createSessionOrThrow logged the whole request at
silly, which production wrote, and PayPal's whole answer at info. The
webhook logged its whole body, and the controller's webhook error
handler the error object as JSON.

The SDK now logs method, URL and status of each request, without
bodies or headers. createSessionOrThrow logs the shop's order id, the
PayPal order's id and its status, and an error by describeError with
the order id. The webhook logs the event type and the id and status of
its resource; its error handler describes the error.

Refs libretech/mp#71
Refs libretech/mp#67
This commit is contained in:
libretech committed 2026-10-09 01:51:23 +02:00
1 parent ba110a11d8
commit b67150376d
2 files changed
+27 -21

No files matched your search

+1 -1
View File
@@ -358,7 +358,7 @@ export default factories.createCoreController("api::order.order", ({ strapi }) =
try {
return await paypalApi.handleWebhook(ctx.request.body);
} catch (error) {
strapi.log.error(`paypal-api: ${JSON.stringify({ error })}`);
strapi.log.error("app:e:order-controller: Handling the PayPal webhook failed", { error: describeError(error) });
return ctx.internalServerError();
}
}
+26 -20
View File
@@ -16,10 +16,12 @@ class PayPalApi {
},
timeout: 0,
environment,
// The SDK logs method, URL and status of each request to the console. Not the bodies: a request body holds the buyer's name
// and address. Not the headers: they hold the token.
logging: {
logLevel: LogLevel.Info,
logRequest: { logBody: true },
logResponse: { logHeaders: true }
logRequest: { logBody: false, logHeaders: false },
logResponse: { logBody: false, logHeaders: false }
}
});
this.ordersController = new OrdersController(this.client);
@@ -29,28 +31,25 @@ class PayPalApi {
* Creates the PayPal order for the order and returns PayPal's id for it, which the capture accepts, and its links. Throws
* CheckoutNotReadyError or ProductUnavailableError before PayPal is asked, for an order PayPal cannot take.
*/
async createSessionOrThrow(returnUrl: string, order: PayPalOrderSource): Promise<{ id: string; links: unknown }> {
async createSessionOrThrow(returnUrl: string, order: PayPalOrderSource & { id?: number | string }): Promise<{ id: string; links: unknown }> {
// Outside the try block: an order that is not ready is the customer's to fix, not a PayPal failure.
const request = buildPayPalOrderRequest(returnUrl, order) as OrderRequest;
try {
const collect = { body: request, prefer: "return=minimal" };
const { body } = await this.ordersController.ordersCreate({ body: request, prefer: "return=minimal" });
const session = JSON.parse(body as string);
if (typeof session?.id !== "string" || session.id === "") throw new Error("PayPal answered without an order id");
strapi.log.silly("app:d:paypal-api: Creating PayPal order " + JSON.stringify({ collect }));
const { body, ...httpResponse } = await this.ordersController.ordersCreate(collect);
const sessionData = JSON.parse(body as string);
strapi.log.info("app:i:paypal-api: " + JSON.stringify({ session: sessionData }));
if (typeof sessionData?.id !== "string" || sessionData.id === "") throw new Error("PayPal answered without an order id");
return {
id: sessionData.id,
links: sessionData.links
};
// The PayPal order's id and status only: the request and PayPal's order hold the buyer's name and address.
strapi.log.info("app:i:paypal-api: Created the PayPal order", {
order: order.id,
paypalOrderId: session.id,
status: typeof session.status === "string" ? session.status : undefined
});
return { id: session.id, links: session.links };
} catch (error) {
// Never the error object: an SDK or axios error carries the request with its Authorization header.
strapi.log.error("app:e:paypal-api: Creating the PayPal order failed", { error: describeError(error) });
strapi.log.error("app:e:paypal-api: Creating the PayPal order failed", { order: order.id, error: describeError(error) });
throw error;
}
}
@@ -71,9 +70,16 @@ class PayPalApi {
return JSON.parse(body as string);
}
async handleWebhook(req: any) {
// Implement PayPal webhook validation and processing
strapi.log.verbose("app:i:paypal-api: " + JSON.stringify({ req }));
async handleWebhook(req: unknown) {
// Implement PayPal webhook validation and processing. The event's resource holds the buyer's name and address, so only the
// event type and the resource's id and status are logged.
const event = (typeof req === "object" && req !== null ? req : {}) as { event_type?: unknown; resource?: { id?: unknown; status?: unknown } };
const textOf = (value: unknown) => (typeof value === "string" ? value : undefined);
strapi.log.verbose("app:v:paypal-api: Webhook received", {
eventType: textOf(event.event_type),
resource: textOf(event.resource?.id),
status: textOf(event.resource?.status)
});
// Note: PayPal webhook handling requires specific implementation
// This is a placeholder and should be replaced with actual PayPal webhook validation