Skip to content

Add standalone PDF engine operations#75

Merged
Jaben merged 3 commits intodevelopfrom
feature/standalone-pdf-operations
Mar 29, 2026
Merged

Add standalone PDF engine operations#75
Jaben merged 3 commits intodevelopfrom
feature/standalone-pdf-operations

Conversation

@Jaben
Copy link
Copy Markdown
Member

@Jaben Jaben commented Mar 28, 2026

Summary

  • New PdfEngineRequest base class with concrete types: FlattenPdfRequest, RotatePdfRequest, SplitPdfRequest, EncryptPdfRequest, ReadMetadataRequest, WriteMetadataRequest
  • PdfEngineBuilders static factory with fluent creation: Flatten(), Rotate(90), Split(mode, span), Encrypt(user, owner), ReadMetadata(), WriteMetadata(dict)
  • Add ExecutePdfEngineAsync and ReadPdfMetadataAsync (returns JSON string) to GotenbergSharpClient
  • API paths: /forms/pdfengines/flatten, /rotate, /split, /encrypt, /metadata/read, /metadata/write, /watermark, /stamp

Test plan

  • 13 tests covering builder factories, validation, and integration
  • 5 integration tests: flatten, rotate, encrypt, write metadata, read metadata (returns parsed JSON)
  • PdfEngineOperations example console app added
  • README section with usage for all operations

Summary by CodeRabbit

  • New Features

    • Added support for standalone PDF engine operations: Flatten, Rotate, Split, Encrypt, and Metadata Read/Write functionality.
    • Introduced new API methods for executing PDF engine operations with builder pattern support.
    • Released new example project demonstrating practical usage of PDF engine operations.
  • Documentation

    • Enhanced README with detailed code examples and guide for using PDF engine operations.

…, metadata)

Introduce PdfEngineRequest base class and concrete request types:
FlattenPdfRequest, RotatePdfRequest, SplitPdfRequest, EncryptPdfRequest,
ReadMetadataRequest, WriteMetadataRequest. Each maps to a standalone
/forms/pdfengines/* route.

Add PdfEngineBuilders static factory with fluent builder creation for
each operation. Add ExecutePdfEngineAsync and ReadPdfMetadataAsync
methods to GotenbergSharpClient.

Include RotationAngle, PageRanges, and SplitMode value objects for
domain validation. Add PdfEngineOperations example and README section.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Warning

Rate limit exceeded

@Jaben has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 7 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 7 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa34f55f-c2f5-4374-8113-2a6e396fe45a

📥 Commits

Reviewing files that changed from the base of the PR and between d9da3b5 and b5bbb3a.

📒 Files selected for processing (8)
  • examples/PdfEngineOperations/Program.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/EncryptPdfRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/FlattenPdfRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/WriteMetadataRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/PageRanges.cs
  • src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs
  • test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs
📝 Walkthrough

Walkthrough

Added standalone PDF engine operations support to GotenbergSharpClient with new request types (Flatten, Rotate, Split, Encrypt, ReadMetadata, WriteMetadata), builder factory classes, value objects for rotation angles and page ranges, API client methods, infrastructure constants, comprehensive tests, documentation, and a working example application.

Changes

Cohort / File(s) Summary
Documentation & Project Setup
README.md, examples/PdfEngineOperations/PdfEngineOperations.csproj
Added documentation of standalone PDF engine operations with example C# usage, and created new example project structure.
Example Application
examples/PdfEngineOperations/Program.cs
Added complete working example demonstrating PDF engine operations (flatten, rotate, encrypt, metadata read/write) with HTML-to-PDF generation, timestamped file output, and JSON metadata parsing.
PDF Engine Request Types
src/Gotenberg.Sharp.Api.Client/Domain/Requests/PdfEngineRequest.cs, FlattenPdfRequest.cs, RotatePdfRequest.cs, SplitPdfRequest.cs, EncryptPdfRequest.cs, ReadMetadataRequest.cs, WriteMetadataRequest.cs
Introduced abstract base class and seven concrete request types for PDF engine operations, each targeting specific API endpoints with operation-specific validation and form data serialization.
Value Objects
src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/RotationAngle.cs, PageRanges.cs, SplitMode.cs
Added three new value objects: RotationAngle (restricted to 90/180/270 degrees), PageRanges (validated range strings via regex), and SplitMode enum (Intervals/Pages) with form serialization support.
Builder Factory Classes
src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs
Introduced generic PdfEngineBuilder base class with asset management and PdfEngineBuilders static factory providing methods for Flatten, Rotate, Split, Encrypt, ReadMetadata, and WriteMetadata operations.
Client API Methods
src/Gotenberg.Sharp.Api.Client/GotenbergSharpClient.cs
Added three public async methods: ExecutePdfEngineAsync (direct request), ExecutePdfEngineAsync (builder-based), and ReadPdfMetadataAsync for PDF metadata retrieval with conditional compilation for NET5\_0\_OR\_GREATER.
Infrastructure Constants
src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs
Extended Gotenberg.PdfEngines.ApiPaths with eight new route constants for Flatten, Rotate, Split, Encrypt, ReadMetadata, WriteMetadata, Watermark, and Stamp endpoints.
Comprehensive Tests
test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs
Added builder construction tests and integration tests validating correct request generation, error handling, invalid input rejection, and end-to-end PDF engine operation execution with metadata parsing.

Sequence Diagram

sequenceDiagram
    participant Client
    participant PdfEngineBuilders
    participant PdfEngineBuilder
    participant GotenbergSharpClient
    participant Gotenberg as Gotenberg API

    Client->>PdfEngineBuilders: PdfEngineBuilders.Rotate(angle)
    PdfEngineBuilders->>PdfEngineBuilder: Create RotatePdfRequest
    PdfEngineBuilder-->>Client: PdfEngineBuilder<RotatePdfRequest>
    
    Client->>PdfEngineBuilder: WithPdfs(AssetBuilder)
    PdfEngineBuilder->>PdfEngineBuilder: Set Assets
    PdfEngineBuilder-->>Client: PdfEngineBuilder<RotatePdfRequest>
    
    Client->>GotenbergSharpClient: ExecutePdfEngineAsync(builder)
    GotenbergSharpClient->>PdfEngineBuilder: BuildAsync()
    PdfEngineBuilder->>PdfEngineBuilder: Validate()
    PdfEngineBuilder-->>GotenbergSharpClient: RotatePdfRequest
    
    GotenbergSharpClient->>Gotenberg: POST /forms/pdfengines/rotate
    Gotenberg-->>GotenbergSharpClient: Stream (rotated PDF bytes)
    GotenbergSharpClient-->>Client: Task<Stream>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

  • Moving "/lib" to "/src" #63: Modifies the same Constants.cs file with additions to Gotenberg.PdfEngines ApiPaths for PDF engine endpoints.

Poem

🐰 hops excitedly
PDFs now rotate, flatten, and gleam,
Builders that craft every developer's dream,
Metadata reads with a hop and a bound,
Sharp engines make PDF magic abound! 📄

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add standalone PDF engine operations' clearly and concisely summarizes the main change in the PR, which introduces new standalone PDF engine functionality including request types, builders, and client methods.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/standalone-pdf-operations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Jaben
Copy link
Copy Markdown
Member Author

Jaben commented Mar 29, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
src/Gotenberg.Sharp.Api.Client/Domain/Requests/FlattenPdfRequest.cs (1)

8-11: Consider adding XML documentation for consistency.

Other new request types like ReadMetadataRequest include XML documentation (/// <summary>). Adding similar documentation here would maintain consistency across the request types.

📝 Suggested documentation
+/// <summary>
+/// Flattens PDF form fields and annotations.
+/// </summary>
 public sealed class FlattenPdfRequest : PdfEngineRequest
 {
     protected override string ApiPath => Constants.Gotenberg.PdfEngines.ApiPaths.Flatten;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Requests/FlattenPdfRequest.cs` around
lines 8 - 11, Add XML documentation to FlattenPdfRequest to match other request
types: add a /// <summary> describing that FlattenPdfRequest represents a
request to the Gotenberg PDF engine to flatten PDFs, and document the overridden
ApiPath property (or mention it in the class summary). Update the
FlattenPdfRequest class (which inherits PdfEngineRequest and overrides ApiPath)
to include these XML comments and keep wording consistent with
ReadMetadataRequest and other request classes.
src/Gotenberg.Sharp.Api.Client/Domain/Requests/EncryptPdfRequest.cs (1)

16-20: Use a consistent exception type for invalid UserPassword.

Builder validation throws ArgumentException, while request validation throws InvalidOperationException for the same precondition. Aligning these improves API predictability.

♻️ Proposed fix
 if (string.IsNullOrWhiteSpace(this.UserPassword))
-    throw new InvalidOperationException("User password is required for encryption.");
+    throw new ArgumentException("User password is required for encryption.", nameof(UserPassword));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Requests/EncryptPdfRequest.cs` around
lines 16 - 20, The EncryptPdfRequest.Validate method currently throws
InvalidOperationException when UserPassword is null/whitespace; change this to
throw ArgumentException to match the builder validation behavior. Locate the
EncryptPdfRequest class and its Validate() override, replace the
InvalidOperationException thrown for this.UserPassword with an ArgumentException
(include a clear message and set the parameter name to "UserPassword"), so
request-level validation uses the same exception type as the builder.
examples/PdfEngineOperations/Program.cs (1)

29-58: Dispose response streams explicitly in the example flow.

Streams from ExecutePdfEngineAsync and HtmlToPdfAsync are currently left undisposed. Explicit using avoids leaks and sets a better usage pattern for consumers.

♻️ Proposed pattern
- var flattenResult = await sharpClient.ExecutePdfEngineAsync(...);
- await SaveStream(flattenResult, destinationDirectory, "Flattened.pdf");
+ using (var flattenResult = await sharpClient.ExecutePdfEngineAsync(...))
+     await SaveStream(flattenResult, destinationDirectory, "Flattened.pdf");

- var stream = await client.HtmlToPdfAsync(builder);
+ using var stream = await client.HtmlToPdfAsync(builder);

Also applies to: 74-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/PdfEngineOperations/Program.cs` around lines 29 - 58, The example
leaves response streams from ExecutePdfEngineAsync and ReadPdfMetadataAsync (and
similar calls like HtmlToPdfAsync) undisposed; wrap each call's returned
stream/result in a proper using (or await using for IAsyncDisposable) block so
the stream is passed to SaveStream while guaranteed to be disposed afterwards;
update the flattenResult, rotateResult, encryptResult, writeResult, metadataJson
(and the similar block around HtmlToPdfAsync at the other location) to use
using/await using around the call and then call SaveStream/read within that
scope to prevent leaks and show the proper consumption pattern.
test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs (2)

218-222: Dispose the HTML->PDF stream in GenerateTestPdf.

The source stream returned by HtmlToPdfAsync is copied but never disposed.

♻️ Proposed fix
- var stream = await client.HtmlToPdfAsync(builder);
+ using var stream = await client.HtmlToPdfAsync(builder);

  using var ms = new MemoryStream();
  await stream.CopyToAsync(ms);
  return ms.ToArray();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs` around lines 218
- 222, The returned source stream from HtmlToPdfAsync is not disposed in
GenerateTestPdf; update GenerateTestPdf to dispose the stream by wrapping the
call in a using (or using var) — e.g. using var stream = await
client.HtmlToPdfAsync(builder); — before copying to MemoryStream and returning
ms.ToArray(); this ensures the Stream returned by HtmlToPdfAsync is properly
disposed.

125-193: Dispose result streams in integration tests.

Each ExecutePdfEngineAsync call returns a stream that should be disposed after assertions to avoid unnecessary resource retention in the suite.

♻️ Proposed pattern
- var result = await client.ExecutePdfEngineAsync(builder);
+ using var result = await client.ExecutePdfEngineAsync(builder);

  result.Should().NotBeNull();
  result.Length.Should().BeGreaterThan(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs` around lines 125
- 193, The tests call ExecutePdfEngineAsync(builder) which returns a stream
(variable result) that is not disposed; update each test using
ExecutePdfEngineAsync (e.g., the tests RotatePdf_Succeeds, EncryptPdf_Succeeds,
WriteMetadata_Succeeds and any similar tests) to dispose the returned stream
after assertions by wrapping the result in a using block or explicitly calling
Dispose() (ensure assertions still run while the stream is open). Do not change
ReadPdfMetadataAsync usage (it returns a string), and make the change for every
occurrence of ExecutePdfEngineAsync(builder) in the test file.
src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs (1)

98-107: Consider validating span for empty/whitespace strings for consistency.

The Encrypt method (line 114-115) validates that userPassword is not null or whitespace, but Split only checks for null on span. If an empty or whitespace span value would cause an API error from Gotenberg, it would be better to fail fast here.

🔧 Suggested validation enhancement
 public static PdfEngineBuilder<SplitPdfRequest> Split(SplitMode mode, string span, bool unify = false)
 {
+    if (string.IsNullOrWhiteSpace(span))
+        throw new ArgumentException("Span is required.", nameof(span));
+
     var request = new SplitPdfRequest
     {
         Mode = mode,
-        Span = span ?? throw new ArgumentNullException(nameof(span)),
+        Span = span,
         Unify = unify
     };
     return new PdfEngineBuilder<SplitPdfRequest>(request);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs` around
lines 98 - 107, The Split method currently only checks span for null; update
validation to mirror Encrypt by treating empty or whitespace spans as invalid:
in PdfEngineBuilder<SplitPdfRequest>.Split check string.IsNullOrWhiteSpace(span)
and throw an ArgumentException (or ArgumentNullException with a clear message)
if true, then assign Span when valid; reference Split, SplitPdfRequest and
PdfEngineBuilder to locate the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs`:
- Around line 49-56: PdfEngineBuilder<TRequest>.WithPdfsAsync currently invokes
asyncAction immediately (via asyncAction(new AssetBuilder(this.Request.Assets
??= new AssetDictionary()))) which creates shared AssetDictionary and produces
Tasks that run concurrently at BuildAsync, causing race conditions; change
WithPdfsAsync so it does not call asyncAction eagerly but instead adds a
deferred wrapper to BuildTasks (e.g., a Func<Task> or a Task-returning lambda)
that creates the AssetBuilder from Request.Assets ??= new AssetDictionary() and
invokes asyncAction only when BuildAsync executes, or alternatively replace
AssetDictionary with a thread-safe collection; update related builders
(BaseChromiumBuilder, PdfConversionBuilder, BaseMergeBuilder) to the same
deferred-invocation pattern or thread-safe storage to eliminate concurrent
writes.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Requests/WriteMetadataRequest.cs`:
- Around line 27-36: WriteMetadataRequest::ToHttpContent is incorrectly marking
JSON metadata as text/html; add an ApplicationJson constant to
Constants.HttpContent.MediaTypes and change the ContentType used in
WriteMetadataRequest.ToHttpContent from MediaTypes.TextHtml to
MediaTypes.ApplicationJson; also update MultiFormHeaderAttribute (which
currently defaults to TextHtml) to default to ApplicationJson so multipart form
parts carrying JSON metadata are correctly labeled.

In `@src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/PageRanges.cs`:
- Around line 26-55: The regex-only validation in PageRanges.Create permits
semantically invalid ranges like "0" or descending ranges "5-2"; after the
existing regex check, parse the trimmed string (ranges.Split(',')) and for each
segment (use the same trimming logic) validate that standalone numbers are >=1
and that hyphenated ranges parse to two integers both >=1 with start <= end,
throwing ArgumentException with a clear message if any check fails; keep
returning new PageRanges(trimmed) only after all semantic checks pass and
reference the ValidPattern, Create method, and PageRanges constructor/Value to
locate the changes.

---

Nitpick comments:
In `@examples/PdfEngineOperations/Program.cs`:
- Around line 29-58: The example leaves response streams from
ExecutePdfEngineAsync and ReadPdfMetadataAsync (and similar calls like
HtmlToPdfAsync) undisposed; wrap each call's returned stream/result in a proper
using (or await using for IAsyncDisposable) block so the stream is passed to
SaveStream while guaranteed to be disposed afterwards; update the flattenResult,
rotateResult, encryptResult, writeResult, metadataJson (and the similar block
around HtmlToPdfAsync at the other location) to use using/await using around the
call and then call SaveStream/read within that scope to prevent leaks and show
the proper consumption pattern.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs`:
- Around line 98-107: The Split method currently only checks span for null;
update validation to mirror Encrypt by treating empty or whitespace spans as
invalid: in PdfEngineBuilder<SplitPdfRequest>.Split check
string.IsNullOrWhiteSpace(span) and throw an ArgumentException (or
ArgumentNullException with a clear message) if true, then assign Span when
valid; reference Split, SplitPdfRequest and PdfEngineBuilder to locate the code
to change.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Requests/EncryptPdfRequest.cs`:
- Around line 16-20: The EncryptPdfRequest.Validate method currently throws
InvalidOperationException when UserPassword is null/whitespace; change this to
throw ArgumentException to match the builder validation behavior. Locate the
EncryptPdfRequest class and its Validate() override, replace the
InvalidOperationException thrown for this.UserPassword with an ArgumentException
(include a clear message and set the parameter name to "UserPassword"), so
request-level validation uses the same exception type as the builder.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Requests/FlattenPdfRequest.cs`:
- Around line 8-11: Add XML documentation to FlattenPdfRequest to match other
request types: add a /// <summary> describing that FlattenPdfRequest represents
a request to the Gotenberg PDF engine to flatten PDFs, and document the
overridden ApiPath property (or mention it in the class summary). Update the
FlattenPdfRequest class (which inherits PdfEngineRequest and overrides ApiPath)
to include these XML comments and keep wording consistent with
ReadMetadataRequest and other request classes.

In `@test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs`:
- Around line 218-222: The returned source stream from HtmlToPdfAsync is not
disposed in GenerateTestPdf; update GenerateTestPdf to dispose the stream by
wrapping the call in a using (or using var) — e.g. using var stream = await
client.HtmlToPdfAsync(builder); — before copying to MemoryStream and returning
ms.ToArray(); this ensures the Stream returned by HtmlToPdfAsync is properly
disposed.
- Around line 125-193: The tests call ExecutePdfEngineAsync(builder) which
returns a stream (variable result) that is not disposed; update each test using
ExecutePdfEngineAsync (e.g., the tests RotatePdf_Succeeds, EncryptPdf_Succeeds,
WriteMetadata_Succeeds and any similar tests) to dispose the returned stream
after assertions by wrapping the result in a using block or explicitly calling
Dispose() (ensure assertions still run while the stream is open). Do not change
ReadPdfMetadataAsync usage (it returns a string), and make the change for every
occurrence of ExecutePdfEngineAsync(builder) in the test file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f3dcaaf-63e5-4c94-bf97-14704f2b1001

📥 Commits

Reviewing files that changed from the base of the PR and between 09b6c02 and d9da3b5.

📒 Files selected for processing (17)
  • README.md
  • examples/PdfEngineOperations/PdfEngineOperations.csproj
  • examples/PdfEngineOperations/Program.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/EncryptPdfRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/FlattenPdfRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/PdfEngineRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/ReadMetadataRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/RotatePdfRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/SplitPdfRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/Requests/WriteMetadataRequest.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/PageRanges.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/RotationAngle.cs
  • src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/SplitMode.cs
  • src/Gotenberg.Sharp.Api.Client/GotenbergSharpClient.cs
  • src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs
  • test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs

Comment on lines +49 to +56
public PdfEngineBuilder<TRequest> WithPdfsAsync(Func<AssetBuilder, Task> asyncAction)
{
if (asyncAction == null) throw new ArgumentNullException(nameof(asyncAction));

this.BuildTasks.Add(asyncAction(new AssetBuilder(this.Request.Assets ??= new AssetDictionary())));

return this;
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if AssetDictionary has any thread-safety mechanisms or if other async builders use the same pattern
rg -n "BuildTasks\.Add" --type cs -C3

Repository: ChangemakerStudios/GotenbergSharpApiClient

Length of output: 4647


🏁 Script executed:

#!/bin/bash
# Find BuildAsync method to see how tasks are executed
rg -n "BuildAsync" --type cs -A 10 | head -100

Repository: ChangemakerStudios/GotenbergSharpApiClient

Length of output: 9226


🏁 Script executed:

#!/bin/bash
# Check the base class or interface that defines BuildTasks
rg -n "BuildTasks" --type cs -B 3 -A 1 | head -150

Repository: ChangemakerStudios/GotenbergSharpApiClient

Length of output: 5301


🏁 Script executed:

#!/bin/bash
# Look at AssetDictionary to see if it has any thread-safety mechanisms
rg -n "class AssetDictionary" --type cs -A 10

Repository: ChangemakerStudios/GotenbergSharpApiClient

Length of output: 1395


Potential race condition when multiple WithPdfsAsync calls are chained.

The asyncAction is invoked immediately (line 53), returning a Task that is added to BuildTasks. Multiple calls to WithPdfsAsync share the same AssetDictionary instance (via ??=). When BuildAsync() is called, all tasks execute concurrently through Task.WhenAll, causing concurrent writes to AssetDictionary, which extends Dictionary<string, ContentItem> (not thread-safe).

This pattern exists across the codebase (BaseChromiumBuilder, PdfConversionBuilder, BaseMergeBuilder, and others), suggesting either a widespread architectural issue or missing documentation on non-concurrent usage.

If chaining multiple WithPdfsAsync calls is supported, consider:

  1. Using ConcurrentDictionary<string, ContentItem> for AssetDictionary
  2. Adding documentation requiring sequential (non-concurrent) calls
  3. Deferring asyncAction invocation by wrapping in a lambda, only invoking during BuildAsync()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Builders/PdfEngineBuilder.cs` around
lines 49 - 56, PdfEngineBuilder<TRequest>.WithPdfsAsync currently invokes
asyncAction immediately (via asyncAction(new AssetBuilder(this.Request.Assets
??= new AssetDictionary()))) which creates shared AssetDictionary and produces
Tasks that run concurrently at BuildAsync, causing race conditions; change
WithPdfsAsync so it does not call asyncAction eagerly but instead adds a
deferred wrapper to BuildTasks (e.g., a Func<Task> or a Task-returning lambda)
that creates the AssetBuilder from Request.Assets ??= new AssetDictionary() and
invokes asyncAction only when BuildAsync executes, or alternatively replace
AssetDictionary with a thread-safe collection; update related builders
(BaseChromiumBuilder, PdfConversionBuilder, BaseMergeBuilder) to the same
deferred-invocation pattern or thread-safe storage to eliminate concurrent
writes.

Comment on lines +27 to +36
protected override IEnumerable<HttpContent> ToHttpContent()
{
var metadataContent = new StringContent(this.Metadata!.ToString());
metadataContent.Headers.ContentType = new MediaTypeHeaderValue(Constants.HttpContent.MediaTypes.TextHtml);
metadataContent.Headers.ContentDisposition =
new ContentDispositionHeaderValue(Constants.HttpContent.Disposition.Types.FormData)
{
Name = "metadata"
};
yield return metadataContent;
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Gotenberg PDF engine metadata write endpoint content type requirement

💡 Result:

The Gotenberg PDF engine metadata write endpoint is POST /forms/pdfengines/metadata/write. It requires the Content-Type header to be set to multipart/form-data, as all Gotenberg routes are designed to accept multipart/form-data requests containing form fields and files. Required parts: - Form files: One or more PDF files (named arbitrarily, e.g., files=@pdf.pdf). - Form field: metadata (JSON string, required; e.g., --form 'metadata={"CreateDate": "2025:05:16 22:03:20+02:00"}'). Example from GitHub discussion #1189: curl --request POST http://localhost:3000/forms/pdfengines/metadata/write --form files=@pdfa.pdf --form 'metadata={"CreateDate": "2025:05:16 22:03:20+02:00"}' -o my.pdf Official docs confirm this is a multipart/form-data route (Routes page). Not all metadata keys are writable (see ExifTool XMP PDF tags). Writing metadata may break PDF/A compliance.

Citations:


🏁 Script executed:

# Search for other StringContent usages with TextHtml
rg "MediaTypes\.TextHtml" --type cs

# Search for other StringContent in request classes
rg "StringContent" --type cs -A 3

# Check for WriteMetadataRequest usage/tests
fd -e "*.cs" -x rg -l "WriteMetadataRequest"

Repository: ChangemakerStudios/GotenbergSharpApiClient

Length of output: 2613


🏁 Script executed:

# Check MultiFormHeaderAttribute context
cat -n src/Gotenberg.Sharp.Api.Client/Infrastructure/MultiFormHeaderAttribute.cs | head -30

# Check if there are tests for WriteMetadataRequest
rg -l "WriteMetadataRequest" --type cs

# Look at the full context of BuildRequestBase.CreateFormDataItem
cat -n src/Gotenberg.Sharp.Api.Client/Domain/Requests/BuildRequestBase.cs | grep -A 10 "CreateFormDataItem"

# Check Constants.HttpContent to see what MediaTypes are available
rg "MediaTypes\." --type cs | grep -E "(class|const|=)" | head -20

Repository: ChangemakerStudios/GotenbergSharpApiClient

Length of output: 2968


🏁 Script executed:

# Find Constants file with MediaTypes
fd "Constants.cs" -o fd "Constants" --type f | head -10

# Search for MediaTypes definition
rg "class MediaTypes|namespace.*Constants" --type cs -B 2 -A 15 | head -50

# Check the full Constants definition
rg -B 5 "TextHtml|ApplicationJson" --type cs | grep -E "(const|=|public)" | head -20

# Look at the test file for WriteMetadataRequest
cat test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs | grep -A 30 "WriteMetadata"

Repository: ChangemakerStudios/GotenbergSharpApiClient

Length of output: 5493


Incorrect ContentType for JSON metadata.

The metadata content is JSON (from JObject.ToString()), but ContentType is set to "text/html" instead of "application/json". While integration tests pass (suggesting Gotenberg tolerates this), the content type is semantically incorrect for JSON. Add an ApplicationJson constant to MediaTypes and use it here:

 var metadataContent = new StringContent(this.Metadata!.ToString());
-metadataContent.Headers.ContentType = new MediaTypeHeaderValue(Constants.HttpContent.MediaTypes.TextHtml);
+metadataContent.Headers.ContentType = new MediaTypeHeaderValue(Constants.HttpContent.MediaTypes.ApplicationJson);

Note: MultiFormHeaderAttribute also defaults to TextHtml and should be updated similarly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Gotenberg.Sharp.Api.Client/Domain/Requests/WriteMetadataRequest.cs`
around lines 27 - 36, WriteMetadataRequest::ToHttpContent is incorrectly marking
JSON metadata as text/html; add an ApplicationJson constant to
Constants.HttpContent.MediaTypes and change the ContentType used in
WriteMetadataRequest.ToHttpContent from MediaTypes.TextHtml to
MediaTypes.ApplicationJson; also update MultiFormHeaderAttribute (which
currently defaults to TextHtml) to default to ApplicationJson so multipart form
parts carrying JSON metadata are correctly labeled.

Comment thread src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/PageRanges.cs
@Jaben Jaben merged commit badcdbe into develop Mar 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant