libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit ca9ab03772426ddfaa17492ad85b2667e77fb3bb
parent be8f352a7e202e0857a401f277f899ac80ebf9e3
Author: Antoine A <>
Date:   Tue, 15 Jul 2025 19:16:50 +0200

nexus: log http error

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsLogger.kt | 37++++++++++++++++++++++++++++++++-----
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 14+++++++++-----
Mnexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt | 26++++++++++++--------------
Mnexus/src/test/kotlin/EbicsTest.kt | 9+++++----
4 files changed, 58 insertions(+), 28 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsLogger.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsLogger.kt @@ -27,6 +27,7 @@ import java.time.* import java.time.format.DateTimeFormatter import kotlin.io.* import kotlin.io.path.* +import io.ktor.client.statement.* /** Log EBICS transactions steps and payload if [path] is not null */ class EbicsLogger(private val dir: Path?) { @@ -104,13 +105,39 @@ class StepLogger internal constructor( private val dir: Path?, private val name: String? ) { - /** Log a protocol step [request] and [response] */ - fun log(request: ByteArray, response: InputStream): InputStream { + private val prefix = if (name != null) "$name-" else "" + + /** Log a protocol step [request] */ + fun logRequest(request: ByteArray) { + if (dir != null) { + dir.resolve("${prefix}request.xml") + .writeBytes(request, StandardOpenOption.CREATE_NEW) + } + } + + /** Log a protocol step failure */ + suspend fun logFailure(res: HttpResponse) { + if (dir != null) { + // TODO reduce allocation + // TODO silent io error + val bytes = buildString { + append("${res.version} ${res.status}\n") + for ((k, vs) in res.headers.entries()) { + for (v in vs) { + append("${k}: ${v}\n") + } + } + append('\n') + }.toByteArray() + res.readBytes() + dir.resolve("${prefix}failure") + .writeBytes(bytes, StandardOpenOption.CREATE_NEW) + } + } + + /** Log a protocol step [response] */ + fun logResponse(response: InputStream): InputStream { if (dir == null) return response val bytes = response.readBytes() - val prefix = if (name != null) "$name-" else "" - dir.resolve("${prefix}request.xml") - .writeBytes(request, StandardOpenOption.CREATE_NEW) dir.resolve("${prefix}response.xml") .writeBytes(bytes, StandardOpenOption.CREATE_NEW) return bytes.inputStream() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -64,8 +64,9 @@ suspend fun HttpClient.postToBank( bankUrl: String, msg: ByteArray, phase: String, - stepLogger: StepLogger? = null + stepLogger: StepLogger ): Document { + stepLogger.logRequest(msg) val res = try { post(urlString = bankUrl) { contentType(ContentType.Text.Xml) @@ -76,11 +77,12 @@ suspend fun HttpClient.postToBank( } if (res.status != HttpStatusCode.OK) { + stepLogger.logFailure(res) throw EbicsError.HTTP("$phase: bank HTTP error: ${res.status}", res.status) } try { val bodyStream = res.bodyAsChannel().toInputStream() - val loggedStream = stepLogger?.log(msg, bodyStream) ?: bodyStream + val loggedStream = stepLogger.logResponse(bodyStream) return XMLUtil.parseIntoDom(loggedStream) } catch (e: SAXException) { throw EbicsError.Protocol("$phase: invalid XML bank response", e) @@ -94,7 +96,7 @@ suspend fun EbicsBTS.postBTS( client: HttpClient, xmlReq: ByteArray, phase: String, - stepLogger: StepLogger? = null + stepLogger: StepLogger ): BTSResponse { val doc = client.postToBank(cfg.host.baseUrl, xmlReq, phase, stepLogger) try { @@ -165,16 +167,16 @@ class EbicsClient( } } } - val txLog = ebicsLogger.tx(order) val impl = EbicsBTS(cfg.ebics, bankKeys, clientKeys, order) // Close interrupted + val interruptedLog = ebicsLogger.tx("INTD") while (true) { val tId = db.ebics.first() if (tId == null) break val xml = impl.downloadReceipt(tId, false) try { - impl.postBTS(client, xml, "Closing interrupted transaction ${tId}") + impl.postBTS(client, xml, "Closing interrupted transaction ${tId}", interruptedLog.step(tId)) } catch (e: Exception) { when (e) { // Transaction already closed or expired - EBICS protocol error @@ -189,6 +191,8 @@ class EbicsClient( db.ebics.remove(tId) } + val txLog = ebicsLogger.tx(order) + // We need to run the logic in a non-cancelable context because we need to send // a receipt for each open download transaction, otherwise we'll be stuck in an // error loop until the pending transaction timeout. diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt @@ -21,11 +21,8 @@ package tech.libeufin.nexus.test import io.ktor.client.* import tech.libeufin.common.* -import tech.libeufin.nexus.BankPublicKeysFile -import tech.libeufin.nexus.ClientPrivateKeysFile -import tech.libeufin.nexus.NexusEbicsConfig import tech.libeufin.nexus.ebics.* -import tech.libeufin.nexus.logger +import tech.libeufin.nexus.* data class TxCheckResult( var concurrentFetchAndFetch: Boolean = false, @@ -52,37 +49,38 @@ suspend fun txCheck( val result = TxCheckResult() val fetch = EbicsBTS(cfg, bankKeys, clientKeys, fetchOrder) val submit = EbicsBTS(cfg, bankKeys, clientKeys, submitOrder) + val ebicsLogger = EbicsLogger(null).tx("test").step("step") - suspend fun EbicsBTS.close(id: String, phase: String) { + suspend fun EbicsBTS.close(id: String, phase: String, ebicsLogger: StepLogger) { val xml = downloadReceipt(id, false) - postBTS(client, xml, phase) + postBTS(client, xml, phase, ebicsLogger) } - val firstTxId = fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init first fetch") + val firstTxId = fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init first fetch", ebicsLogger) .transactionID!! try { try { - val id = fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init second fetch").transactionID!! + val id = fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init second fetch", ebicsLogger).transactionID!! result.concurrentFetchAndFetch = true - fetch.close(id, "Init second fetch") + fetch.close(id, "Init second fetch", ebicsLogger) } catch (e: EbicsError.Code) {} var paylod = prepareUploadPayload(cfg, clientKeys, bankKeys, ByteArray(2000000).rand()) try { - val submitId = submit.postBTS(client, submit.uploadInitialization(paylod), "Init first submit"). transactionID!! + val submitId = submit.postBTS(client, submit.uploadInitialization(paylod), "Init first submit", ebicsLogger). transactionID!! result.concurrentFetchAndSubmit = true - submit.postBTS(client, submit.uploadTransfer(submitId, paylod, 1), "Submit first upload") + submit.postBTS(client, submit.uploadTransfer(submitId, paylod, 1), "Submit first upload", ebicsLogger) try { - submit.postBTS(client, submit.uploadInitialization(paylod), "Init second submit") + submit.postBTS(client, submit.uploadInitialization(paylod), "Init second submit", ebicsLogger) result.concurrentSubmitAndSubmit = true } catch (e: EbicsError.Code) {} } catch (e: EbicsError.Code) {} } finally { - fetch.close(firstTxId, "Close first fetch") + fetch.close(firstTxId, "Close first fetch", ebicsLogger) } try { - fetch.close(firstTxId, "Close first fetch a second time") + fetch.close(firstTxId, "Close first fetch a second time", ebicsLogger) result.idempotentClose = true } catch (e: Exception) { logger.debug { e.fmt() } diff --git a/nexus/src/test/kotlin/EbicsTest.kt b/nexus/src/test/kotlin/EbicsTest.kt @@ -403,6 +403,7 @@ class EbicsTest { private val nexusCmd = LibeufinNexus() private val bank = EbicsState() private val args = "-L TRACE -c conf/fetch.conf" + private val ebicsLogger = EbicsLogger(null).tx("test").step("step") private fun setMock(sequences: Sequence<(String) -> ByteArray>) { val steps = sequences.iterator() @@ -448,7 +449,7 @@ class EbicsTest { assertFailsWith<EbicsError.HTTP> { getMockedClient { respondError(HttpStatusCode.NotFound) - }.postToBank("http://ignored.example.com/", ByteArray(0), "Test") + }.postToBank("http://ignored.example.com/", ByteArray(0), "Test", ebicsLogger) }.run { assertEquals(HttpStatusCode.NotFound, status) assertEquals("Test: bank HTTP error: 404 Not Found", message) @@ -456,7 +457,7 @@ class EbicsTest { assertFailsWith<EbicsError.Network> { getMockedClient { throw Exception("Simulate failure") - }.postToBank("http://ignored.example.com/", ByteArray(0), "Test") + }.postToBank("http://ignored.example.com/", ByteArray(0), "Test", ebicsLogger) }.run { assertEquals("Test: failed to contact bank", message) assertEquals("Simulate failure", cause!!.message) @@ -464,14 +465,14 @@ class EbicsTest { assertFailsWith<EbicsError.Protocol> { getMockedClient { respondOk("<ebics broken></ebics>") - }.postToBank("http://ignored.example.com/", ByteArray(0), "Test") + }.postToBank("http://ignored.example.com/", ByteArray(0), "Test", ebicsLogger) }.run { assertEquals("Test: invalid XML bank response", message) assertEquals("Attribute name \"broken\" associated with an element type \"ebics\" must be followed by the ' = ' character.", cause!!.message) } getMockedClient { respondOk("<ebics></ebics>") - }.postToBank("http://ignored.example.com/", ByteArray(0), "Test") + }.postToBank("http://ignored.example.com/", ByteArray(0), "Test", ebicsLogger) } // Tests that internal repr. of keys lead to valid PDF.