Andrea Cosentino created CAMEL-26589:
----------------------------------------

             Summary: Data type contract fails on byte[] bodies, breaking 
kamelets with dataTypes
                 Key: CAMEL-26589
                 URL: https://issues.apache.org/jira/browse/CAMEL-26589
             Project: Camel
          Issue Type: Bug
          Components: camel-kamelet, camel-yaml-dsl, camel-core
    Affects Versions: 4.19.0, 4.18.1, 4.22.0
            Reporter: Andrea Cosentino


When a route has a data type contract ({{inputType}} / {{outputType}}) and the 
message body is a Java array such as {{byte[]}}, {{ContractAdvice}} fails the 
exchange even though no conversion is needed:

{noformat}
org.apache.camel.CamelExecutionException: Failed to convert body to 'java:[B' 
content using type conversion for null on the exchange: Exchange[...]
        at 
org.apache.camel.processor.transformer.TypeConverterTransformer.transform(TypeConverterTransformer.java:73)
        at 
org.apache.camel.processor.ContractAdvice.convertIfRequired(ContractAdvice.java:157)
        at 
org.apache.camel.processor.ContractAdvice.doTransform(ContractAdvice.java:127)
        at 
org.apache.camel.processor.ContractAdvice.after(ContractAdvice.java:101)
        ...
Caused by: java.lang.ClassNotFoundException: [B
        at 
org.apache.camel.impl.engine.DefaultClassResolver.resolveMandatoryClass(DefaultClassResolver.java:136)
        at 
org.apache.camel.processor.transformer.TypeConverterTransformer.transform(TypeConverterTransformer.java:61)
{noformat}

Since CAMEL-23174 (4.18.1, 4.19.0), {{KameletRoutesBuilderLoader}} turns a 
Kamelet's {{spec.dataTypes.in/out}} into the route {{inputType}} / 
{{outputType}}. Every Kamelet that declares {{dataTypes}} therefore hits this 
whenever its body is an array, which is the normal case for binary sources such 
as {{aws-s3-source}}.

h3. Cause

# When a route has a contract, {{RouteReifier}} calls 
{{camelContext.setUseDataType(true)}}. From then on {{MessageSupport.setBody}} 
tags every body with {{new DataType(body.getClass())}}. For a {{byte[]}} body 
that is {{java:\[B}}, because {{byte[].class.getName()}} is {{\[B}}.
# {{ContractAdvice.after}} -> {{doTransform}} -> {{convertIfRequired(message, 
from)}} with {{from = java:\[B}} builds a 
{{TypeConverterTransformer(DataType)}}, which calls 
{{ClassResolver.resolveMandatoryClass("\[B")}} before it checks whether the 
body already has that type.
# {{DefaultClassResolver}} -> {{ObjectHelper.loadClass}} only uses 
{{ClassLoader.loadClass(name)}}, which cannot load an array class from its 
binary name ({{Class.forName("\[B", false, loader)}} can). 
{{ObjectHelper.loadSimpleType}} knows {{byte[]}} but not {{\[B}}. The lookup 
fails and the exchange is marked failed.

This logic is unchanged between 4.18.0 and 4.22.0 (code inspection), so 
explicit route contracts with array bodies presumably fail on older versions 
too. What CAMEL-23174 changed is that every Kamelet declaring {{dataTypes}} now 
gets such a contract.

h3. Reproducer (Camel 4.22.0, plain JVM)

{code:java}
from("direct:contract").outputType("application-octet-stream").log("in contract 
route");

Exchange e = template.send("direct:contract", ex -> 
ex.getMessage().setBody("hello".getBytes()));
// e.getException(): CamelExecutionException: Failed to convert body to 
'java:[B' ... Caused by: ClassNotFoundException: [B
// the same route with a String body succeeds
{code}

Kamelet variant, loaded from {{classpath:kamelets/bytes-source.kamelet.yaml}} 
and consumed with {{from("kamelet:bytes-source")}}. Sending a {{byte[]}} to 
{{direct:kamelet-in}} fails the same way; a {{String}} body succeeds.

{noformat}
apiVersion: camel.apache.org/v1
kind: Kamelet
metadata:
  name: bytes-source
spec:
  definition:
    title: "Bytes Source"
    properties: {}
  dataTypes:
    out:
      default: binary
      types:
        binary:
          format: "application-octet-stream"
          mediaType: application/octet-stream
  template:
    from:
      uri: "direct:kamelet-in"
      steps:
      - to: "kamelet:sink"
{noformat}

h3. Impact

* The contract is checked when the Kamelet route completes, so the steps after 
{{kamelet:sink}} run first; only then is the exchange marked failed and the 
consumer takes its failure path.
* Verified with {{aws-s3-source}} (Kamelets 4.22.0, default 
{{includeBody=true}}, so the body is {{byte[]}}): every exchange fails at the 
end of the Kamelet route, {{AWS2S3Consumer}} runs {{processRollback}} instead 
of {{processCommit}}, {{deleteAfterRead}} never deletes, and the same objects 
are consumed again on every poll. It is logged at WARN only ("Exchange failed, 
so rolling back message status"), so it is easy to miss. Reported downstream in 
[apache/camel-kafka-connector#1849|https://github.com/apache/camel-kafka-connector/issues/1849].
* 19 Kamelets in the 4.22.0 catalog declare {{dataTypes}}. The sources with a 
binary default output are the most likely to carry {{byte[]}}: aws-s3-source, 
azure-eventhubs-source, azure-storage-blob-source, 
azure-storage-datalake-source, azure-storage-files-source, 
google-storage-source and ibm-cos-source. Only aws-s3-source was verified.

h3. Possible fixes

* Resolve binary array names in {{ObjectHelper.loadClass}} / 
{{DefaultClassResolver}}, for example by falling back to {{Class.forName(name, 
false, loader)}} for names starting with {{\[}}.
* Keep the {{Class}} in {{DataType(Class)}}, so {{TypeConverterTransformer}} 
does not have to resolve it by name.
* Skip the conversion in {{TypeConverterTransformer}} / 
{{ContractAdvice.convertIfRequired}} when the body is already an instance of 
the requested type.

Tests should cover a {{byte[]}} body for both a route contract and a Kamelet 
with {{dataTypes}}.

h3. Workaround

Use the component endpoint directly instead of the Kamelet, so no contract is 
added (verified with {{aws2-s3:...?deleteAfterRead=true}}).

----
_Claude Code on behalf of Andrea Cosentino_




--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to