diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt
index 2b7248a..a310bf2 100644
--- a/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt
+++ b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodec.kt
@@ -1,6 +1,7 @@
package com.gzzn.omms.msgexchange.codec
import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.fasterxml.jackson.dataformat.xml.XmlFactory
import com.fasterxml.jackson.dataformat.xml.XmlMapper
@@ -34,6 +35,14 @@ class JacksonXmlCodec : XmlCodec {
}
val msg = try {
mapper.readValue(trimmed, SisMessageXml::class.java)
+ } catch (e: JsonMappingException) {
+ // META 层绑定失败=发送方违反必填契约(MALFORMED);体层(SCHD/FLOP)结构不匹配
+ // =wire 结构超出编解码器当前绑定能力(CODEC_ERROR:退避重试,修复后可重放,design §2.3)。
+ val first = e.path.firstOrNull()?.fieldName ?: ""
+ if (first.equals("meta", ignoreCase = true)) {
+ return DecodeResult.Err(DecodeFailure(ErrorClass.MALFORMED, "xml-meta:${e.originalMessage.take(200)}"))
+ }
+ return DecodeResult.Err(DecodeFailure(ErrorClass.CODEC_ERROR, "xml-mapping:${e.originalMessage.take(200)}"))
} catch (e: Exception) {
return DecodeResult.Err(
DecodeFailure(ErrorClass.MALFORMED, "xml-parse:${e.message?.take(200) ?: e.javaClass.simpleName}"),
diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
index 386a61e..259187b 100644
--- a/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
+++ b/src/test/kotlin/com/gzzn/omms/msgexchange/codec/JacksonXmlCodecTest.kt
@@ -94,6 +94,26 @@ class JacksonXmlCodecTest {
assertTrue(err.failure.detail.contains("empty-or-non-xml"))
}
+ @Test
+ fun `body shape beyond binding capability is CODEC_ERROR while meta violations stay MALFORMED`() {
+ val meta = "AODB120021010090311SCHDDNLD"
+ val recsAsText = "$metanot-a-number"
+ val bodyErr = codec.decode(recsAsText) as DecodeResult.Err
+ assertEquals(com.gzzn.omms.msgexchange.domain.ErrorClass.CODEC_ERROR, bodyErr.failure.errorClass)
+ assertTrue(bodyErr.failure.detail.startsWith("xml-mapping"))
+
+ val badSeqn = "AODBabc20021010090311SCHDDNLD"
+ val metaErr = codec.decode(badSeqn) as DecodeResult.Err
+ assertEquals(com.gzzn.omms.msgexchange.domain.ErrorClass.MALFORMED, metaErr.failure.errorClass)
+ }
+
+ @Test
+ fun `broken token stream stays MALFORMED`() {
+ val err = codec.decode("") as DecodeResult.Err
+ assertEquals(com.gzzn.omms.msgexchange.domain.ErrorClass.MALFORMED, err.failure.errorClass)
+ assertTrue(err.failure.detail.startsWith("xml-parse"))
+ }
+
@Test
fun `SIS documented attributes and mixed DELY text become typed payload`() {
val raw = """