diff --git a/src/types.ts b/src/types.ts index 08c4300..decd170 100644 --- a/src/types.ts +++ b/src/types.ts @@ -253,12 +253,17 @@ export function createOptionalCallbackFunction( return ((...args: A | [...A, ErrorFirstCallback]) => { const possibleCallback = args[args.length - 1]; if (isErrorFirstCallback(possibleCallback)) { + let result: T; + // Only `syncVersion` may run inside the `try`. Invoking the callback there would let an + // exception thrown *by the callback* land in the `catch` and invoke it a second time. + // https://github.com/node-saml/xml-crypto/issues/527 try { - const result = syncVersion(...(args.slice(0, -1) as A)); - possibleCallback(null, result); + result = syncVersion(...(args.slice(0, -1) as A)); } catch (err) { possibleCallback(err instanceof Error ? err : new Error("Unknown error")); + return; } + possibleCallback(null, result); } else { return syncVersion(...(args as A)); } diff --git a/test/types-tests.spec.ts b/test/types-tests.spec.ts new file mode 100644 index 0000000..348e7ad --- /dev/null +++ b/test/types-tests.spec.ts @@ -0,0 +1,28 @@ +import { SignedXml, type ErrorFirstCallback } from "../src/index"; +import * as fs from "fs"; +import { expect } from "chai"; + +describe("Callback invocation", function () { + // https://github.com/node-saml/xml-crypto/issues/527 + it("invokes the callback once when the callback throws", function () { + const xml = ``; + const sig = new SignedXml(); + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + const errorsSeen: (string | null)[] = []; + const callback: ErrorFirstCallback = (err) => { + errorsSeen.push(err ? err.message : null); + throw new Error("Error Thrown"); + }; + + expect(() => sig.computeSignature(xml, callback)).to.throw("Error Thrown"); + expect(errorsSeen).to.deep.equal([null]); + }); +});