Skip to content

camel-docling: send the bearer token in the Authorization header - #29593

Merged
davsclaus merged 2 commits into
apache:mainfrom
WMK666666:fix-docling-bearer-auth
Nov 12, 2026
Merged

davsclaus merged 2 commits into
apache:mainfrom
WMK666666:fix-docling-bearer-auth

Conversation

@WMK666666

Copy link
Copy Markdown
Contributor

authenticationScheme=BEARER and oauthProfile never sent any credentials at all.

The only authentication the docling-serve client knows is an API key, which it always puts in the X-Api-Key header, and DoclingProducer only called builder.apiKey(...) in the API_KEY branch:

// Apply API key authentication if configured
if (configuration.getAuthenticationScheme() == AuthenticationScheme.API_KEY
        && configuration.getAuthenticationToken() != null) {
    builder.apiKey(configuration.getAuthenticationToken());
}

doclingServeApi = builder.build();

So a BEARER endpoint -- including every endpoint using oauthProfile, because resolveOAuthToken() sets the scheme to BEARER right after it resolves the token -- started up fine, logged with authentication: BEARER, and then sent every request without an Authorization header. docling-serve itself has no built-in auth; it is secured by putting an OAuth proxy in front of it (docling-serve docs), and that proxy wants Authorization: Bearer <token>. So the documented Bearer/OAuth setup produced 401 on every request while the endpoint looked correctly configured, with nothing in the log to explain it.

Fix

The client has no builder option for any other header (DoclingServeApiConfig only carries API_KEY, in 0.7.1 as well), but it does leave createRequestBuilder(RequestContext) open for subclasses. BearerTokenDoclingServeClient extends DoclingServeClient and adds the header there; DoclingProducer uses it when the scheme is BEARER. Everything else -- the HTTP setup, the operations, and the JSON codec -- is the same as the client the builder would have created, so the only difference between an API_KEY and a BEARER endpoint is which header carries the credentials.

getDoclingServeApi() is package private so the producer's wiring can be asserted directly, without a real docling-serve.

Test

DoclingProducerAuthTest starts a stub docling-serve on a loopback port and asserts what actually goes on the wire for each scheme:

scheme Authorization X-Api-Key
BEARER Bearer secret-token absent
API_KEY absent secret-token
NONE absent absent

Reverting only the BEARER branch in DoclingProducer makes exactly the first row fail (expected: "Bearer secret-token" but was: null) and leaves the other two green.

mvn -pl components/camel-ai/camel-docling test
Tests run: 89, Failures: 2, Errors: 0, Skipped: 15

The two failures are DoclingCustomArgsValidationTest.customArgsWithAbsolutePath{,EqualsForm}AreRejected, and they are unrelated and pre-existing on Windows: /etc/cron.d is not an absolute path there, so the absolute-path guard does not fire and the request fails later with an IOException. They fail identically with this change reverted to the original DoclingProducer. Everything else, plus install with formatter, license, enforcer and rat, passes.

Note on the upstream fix

Adding a bearerToken(...) option to ai.docling:docling-serve-client would be the natural place for this, and this subclass could then go away. I am happy to open that upstream and rework this PR on top of a release if you would rather not carry the subclass in the meantime.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In writeValueAsString, JacksonException is caught and re-wrapped in a bare RuntimeException (line 58), while readValue correctly wraps it in JsonReadException (line 46). This asymmetry means callers lose the specific exception type on write failures.

Worth noting: in Jackson 3 (tools.jackson), JacksonException already extends RuntimeException — it's unchecked — so the entire try-catch block is unnecessary. The exception would propagate naturally without any wrapping, preserving the original JacksonException type for callers that care.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Nice fix, and the stub-server test that asserts the actual headers per scheme is the right test. Checked against docling-serve-client 0.7.0: the only auth option is apiKey(...) (sent as X-Api-Key), DoclingServeClientBuilderFactory.newBuilder() picks the Jackson 3 client first, and every request path (executePost, executeGet and both stream variants, so async polling too) goes through createRequestBuilder. So BEARER and oauthProfile really sent nothing before, and the subclass covers all calls.

Small points, none blocking:

  1. BearerTokenDoclingServeClient.java:59-68: as in the open bot thread, drop the try/catch in writeValueAsString. JacksonException is unchecked in Jackson 3 and the upstream Jackson 3 client has no wrapping there, so the class then mirrors it as its Javadoc says.
  2. toBuilder() (line 77) silently drops the token. Camel never calls it and the comment covers it; throwing UnsupportedOperationException would be stricter if you prefer.
  3. This subclasses a protected API of a 0.x library, so a client bump could break it; DoclingProducerAuthTest would catch that. Opening the upstream bearerToken(...) request you mention and linking it from the class Javadoc would make it easy to remove later.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-ai/camel-docling

🔁 1 test passed only after a retry on JDK 21 (1 retried attempt)

Recovered flaky tests on JDK 21 (1)
Module Test Failed attempts First failure
dsl/camel-jbang/camel-jbang-plugin-tui AiPanelAcpTest.retryResendsTheLastQuestionToTheAgent 1 expected: <2> but was: <1>

ℹ️ These tests did not fail the build. Surefire retried them and they passed.
Retries are enabled project-wide by surefire.rerunFailingTestsCount in parent/pom.xml.


🔬 Scalpel shadow comparison — Scalpel: 10 of 722 tested, 27 compile-only — current: 10 all tested

Maveniverse Scalpel detected 10 affected modules (current approach: 10).

Skip-tests mode would test 10 modules (1 direct + 9 downstream), skip tests for 27 (generated code, meta-modules)

Modules Scalpel would test (10)
  • camel-docling ← components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/BearerTokenDoclingServeClient.java, components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java, components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingProducerAuthTest.java
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-jbang-run ← downstream of org.apache.camel:camel-catalog
  • camel-langchain4j-ingest ← downstream of org.apache.camel:camel-docling
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (37 modules, 6m 28s total)

Total reactor time: 6m 28s

Module Duration Status
Camel :: JBang :: MCP 56.9s SUCCESS
Camel :: AI :: LangChain4j :: Ingest 32.7s SUCCESS
Camel :: Component DSL 31.6s SUCCESS
Camel :: YAML DSL :: Validator 29.3s SUCCESS
Camel :: JBang :: Plugin :: Validate 26.9s SUCCESS
Camel :: JBang :: Run 25.8s SUCCESS
Camel :: Catalog :: Camel Catalog 22.4s SUCCESS
Camel :: AI :: Docling 22.0s SUCCESS
Camel :: YAML DSL 22.0s SUCCESS
Camel :: Kamelet Main 20.3s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 16.9s SUCCESS
Camel :: Docs 12.1s SUCCESS
Camel :: JBang :: Plugin :: Testing 11.9s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 7.7s SUCCESS
Camel :: YAML DSL :: Deserializers 7.2s SUCCESS
Camel :: Catalog :: Camel Route Parser 6.8s SUCCESS
Camel :: JBang :: Plugin :: Generate 5.8s SUCCESS
Camel :: All Components Sync point 5.8s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 3.5s SUCCESS
Camel :: Assembly 3.5s SUCCESS
Camel :: YAML DSL :: Maven Plugins 2.8s SUCCESS
Camel :: Catalog :: Maven 2.6s SUCCESS
Camel :: JBang :: Plugin :: MCP 1.8s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 1.8s SUCCESS
Camel :: Coverage 1.7s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.3s SUCCESS
Camel :: Catalog :: Dummy Component 1.0s SUCCESS
Camel :: JBang :: Integration tests 1.0s SUCCESS
Camel :: Endpoint DSL :: Support 0.9s SUCCESS
Camel :: Catalog :: Console 0.8s SUCCESS
Camel :: JBang :: Main 0.8s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a
Camel :: Launcher n/a

Top 20 slowest modules:

  • Camel :: JBang :: MCP (56.9s)
  • Camel :: AI :: LangChain4j :: Ingest (32.7s)
  • Camel :: Component DSL (31.6s)
  • Camel :: YAML DSL :: Validator (29.3s)
  • Camel :: JBang :: Plugin :: Validate (26.9s)
  • Camel :: JBang :: Run (25.8s)
  • Camel :: Catalog :: Camel Catalog (22.4s)
  • Camel :: AI :: Docling (22.0s)
  • Camel :: YAML DSL (22.0s)
  • Camel :: Kamelet Main (20.3s)
  • Camel :: JBang :: Plugin :: Kubernetes (16.9s)
  • Camel :: Docs (12.1s)
  • Camel :: JBang :: Plugin :: Testing (11.9s)
  • Camel :: Catalog :: Camel Report Maven Plugin (7.7s)
  • Camel :: YAML DSL :: Deserializers (7.2s)
  • Camel :: Catalog :: Camel Route Parser (6.8s)
  • Camel :: JBang :: Plugin :: Generate (5.8s)
  • Camel :: All Components Sync point (5.8s)
  • Camel :: YAML DSL :: Validator Maven Plugin (3.5s)
  • Camel :: Assembly (3.5s)

⚙️ View full build and test results

@oscerd oscerd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real bug: configuring authenticationScheme=BEARER (or an oauthProfile, which resolves to a bearer token) sent no credentials at all, because the producer only called builder.apiKey(...) in the API_KEY branch and the docling-serve client has no bearer option. So a user who thought they had secured the call was sending it unauthenticated, which is worse than an error.

Adding the Authorization: Bearer <token> header through the BearerTokenDoclingServeClient subclass is a reasonable way around the SDK not exposing it, and the comment explaining that the token is not part of DoclingServeApiConfig so a builder cannot carry it is the right justification for the subclass rather than a config tweak. I checked that the credential is handled correctly: authenticationToken is @UriParam(label = "security") with security = "secret" and shows secret: true in the catalog, so it is masked in logs and, with #29597, on the TUI screens. The tests pin both schemes and, importantly, that they do not cross over: BEARER sets Authorization and no X-Api-Key, API_KEY sets X-Api-Key and no Authorization. LGTM.

Claude Code on behalf of oscerd

@WMK666666

Copy link
Copy Markdown
Contributor Author

Both points are in 55924d3.

writeValueAsString no longer wraps: JacksonException is unchecked in Jackson 3, so it propagates as it is, which is what the Jackson 3 client of the builder does too. readValue still throws JsonReadException, which it does on purpose so callers can tell a malformed response apart.

I also linked the upstream request from the class Javadoc — docling-project/docling-java#785 adds a bearerToken option to DoclingServeClient, so the subclass can be replaced by the client option once it lands, and the link makes that easy to find.

I left toBuilder() as it is: it does not carry the token, which Camel never calls (the comment says so), and throwing would only turn a dormant path into a runtime surprise.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 55924d3, which is on top of the a9c1d2d I approved.

  • BearerTokenDoclingServeClient.java:63-69: writeValueAsString no longer wraps the exception, so Jackson 3's unchecked JacksonException propagates the same way it does in the upstream Jackson 3 client. This fixes the open bot thread and my point 1. readValue still maps to JsonReadException, which is right.
  • The class Javadoc now links docling-project/docling-java#785 (open, adds bearerToken to the client), so the subclass is easy to find and remove later (my point 3).
  • toBuilder() is unchanged, with a reason given. That is fine: it was optional, and the comment explains it.

No other changes. Still LGTM, pending the queued CI run on 55924d3.

Claude Code on behalf of Claus Ibsen. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

authenticationScheme=BEARER and oauthProfile never sent any credentials. The
only authentication the docling-serve client knows is an API key, which it
always puts in the X-Api-Key header, and DoclingProducer only called
builder.apiKey(...) in the API_KEY branch:

    if (configuration.getAuthenticationScheme() == AuthenticationScheme.API_KEY
            && configuration.getAuthenticationToken() != null) {
        builder.apiKey(configuration.getAuthenticationToken());
    }
    doclingServeApi = builder.build();

So a BEARER endpoint - including every endpoint using oauthProfile, because
resolveOAuthToken() sets the scheme to BEARER after it resolves the token -
started up, logged "with authentication: BEARER", and then sent every request
without an Authorization header. Against docling-serve behind an OAuth proxy
(which is how docling-serve is secured, it has no built-in auth) each request
failed with 401 while the endpoint looked correctly configured.

The client offers no builder option for any other header, but it leaves
createRequestBuilder(RequestContext) open for subclasses. BearerTokenDoclingServeClient
extends DoclingServeClient and adds the header there; DoclingProducer uses it
when the scheme is BEARER. Everything else, including the JSON codec, is the
same as the client the builder would have created.

getDoclingServeApi() is package private so the producer's wiring can be tested
without a real docling-serve.

DoclingProducerAuthTest starts a stub docling-serve and asserts the credentials
on the wire for each scheme: BEARER -> Authorization: Bearer <token> and no
X-Api-Key, API_KEY -> X-Api-Key and no Authorization, NONE -> neither.
…son 3

JacksonException is unchecked in Jackson 3, and the Jackson 3 client that the
builder creates does not wrap it either, so the try-catch only turned a specific
exception into a bare RuntimeException. Drop it, and readValue keeps the
JsonReadException it throws on purpose.

Link the upstream request for a bearer token option so the subclass can be
removed once docling-java offers one.
@davsclaus
davsclaus force-pushed the fix-docling-bearer-auth branch from 55924d3 to 7766cd0 Compare November 11, 2026 20:06
@davsclaus davsclaus added this to the 4.24.0 milestone Nov 12, 2026
@davsclaus davsclaus added the bug Something isn't working label Nov 12, 2026
@davsclaus
davsclaus merged commit 17a3028 into apache:main Nov 12, 2026
6 checks passed
WMK666666 added a commit to WMK666666/camel that referenced this pull request Nov 12, 2026
…e request

Rebased on main now that apache#29591 and apache#29593 are in.

The only conflict was the end of the camel-docling section of the 4.24
upgrade guide, where apache#29591 appended its apiKeyHeader entry: both entries
are kept, the apiKeyHeader one first.

DoclingProducer.java also gained the bearer-token client from apache#29593; it
merged cleanly with the OCR header resolution, which sits in
resolveOcrSettings and applyConfigurationToOptions.

Content is otherwise the one approved in 80d7804: the two headers are
resolved once per message into OcrSettings on the thread that owns the
exchange, so no batch worker touches the exchange, and a language header
does not turn OCR back on when the endpoint disabled it.
WMK666666 added a commit to WMK666666/camel that referenced this pull request Nov 12, 2026
…ocling-serve

Rebased on main now that apache#29591 and apache#29593 are in.

The only conflict was the end of the camel-docling section of the 4.24
upgrade guide, where apache#29591 appended its apiKeyHeader entry: both entries
are kept, the apiKeyHeader one first.

DoclingProducer.java also gained the bearer-token client from apache#29593; it
merged cleanly with the html_split_page rejection in mapToOutputFormat.

Content is otherwise the one reviewed in 1617c89: DoclingContentExtractor
reads doctags_content when DocTags is requested, and mapToOutputFormat
rejects html_split_page with the reason and the way out, because
DocumentResponse has no field for the split pages.
davsclaus pushed a commit that referenced this pull request Nov 12, 2026
camel-docling: honour the OCR headers in the CLI command and the serve request

Rebased on main now that #29591 and #29593 are in.

The only conflict was the end of the camel-docling section of the 4.24
upgrade guide, where #29591 appended its apiKeyHeader entry: both entries
are kept, the apiKeyHeader one first.

DoclingProducer.java also gained the bearer-token client from #29593; it
merged cleanly with the OCR header resolution, which sits in
resolveOcrSettings and applyConfigurationToOptions.

Content is otherwise the one approved in 80d7804: the two headers are
resolved once per message into OcrSettings on the thread that owns the
exchange, so no batch worker touches the exchange, and a language header
does not turn OCR back on when the endpoint disabled it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working components components-ai

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants