From 1ab01179431fab4a4e5cfd9856b831ef10ad1081 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Mon, 30 Mar 2026 18:11:24 +0200 Subject: [PATCH] fix(chromium): assets not loading --- pkg/modules/api/context.go | 17 +++++++++ .../features/chromium_convert_html.feature | 13 +++++++ test/integration/scenario/scenario.go | 34 ++++++++++++++++++ .../testdata/html-with-asset/image.png | Bin 0 -> 69 bytes .../testdata/html-with-asset/index.html | 11 ++++++ 5 files changed, 75 insertions(+) create mode 100644 test/integration/testdata/html-with-asset/image.png create mode 100644 test/integration/testdata/html-with-asset/index.html diff --git a/pkg/modules/api/context.go b/pkg/modules/api/context.go index b5336d3..a4c7692 100644 --- a/pkg/modules/api/context.go +++ b/pkg/modules/api/context.go @@ -473,6 +473,23 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys } } + // Create symlinks from original filenames to UUID-based disk names + // so that relative asset references (e.g., ) + // resolve correctly when Chromium navigates to a file:// URL. + // Symlink creation is best-effort: it may fail for filenames that + // exceed the filesystem NAME_MAX limit (the reason UUIDs were + // introduced in the first place). + for originalName, diskPath := range ctx.files { + symlinkPath := fmt.Sprintf("%s/%s", ctx.dirPath, originalName) + if symlinkPath == diskPath { + continue + } + err = os.Symlink(filepath.Base(diskPath), symlinkPath) + if err != nil { + logger.DebugContext(context.Background(), fmt.Sprintf("skip symlink for '%s': %s", originalName, err)) + } + } + ctx.Log().DebugContext(ctx, fmt.Sprintf("form fields: %+v", ctx.values)) ctx.Log().DebugContext(ctx, fmt.Sprintf("form files: %+v", ctx.files)) ctx.Log().DebugContext(ctx, fmt.Sprintf("form files by field: %+v", ctx.filesByField)) diff --git a/test/integration/features/chromium_convert_html.feature b/test/integration/features/chromium_convert_html.feature index 7409999..a4567f8 100644 --- a/test/integration/features/chromium_convert_html.feature +++ b/test/integration/features/chromium_convert_html.feature @@ -1175,6 +1175,19 @@ Feature: /forms/chromium/convert/html Then the response status code should be 200 Then the response header "Content-Type" should be "application/pdf" + # See: https://github.com/gotenberg/gotenberg/issues/1505. + Scenario: POST /forms/chromium/convert/html (Asset) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/html-with-asset/index.html | file | + | files | testdata/html-with-asset/image.png | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have 1 image(s) + # See: https://github.com/gotenberg/gotenberg/issues/1500. Scenario: POST /forms/chromium/convert/html (Long Filename) Given I have a default Gotenberg container diff --git a/test/integration/scenario/scenario.go b/test/integration/scenario/scenario.go index 4a66238..95dd136 100644 --- a/test/integration/scenario/scenario.go +++ b/test/integration/scenario/scenario.go @@ -952,6 +952,39 @@ func (s *scenario) thePdfShouldHavePages(ctx context.Context, name string, pages return nil } +func (s *scenario) thePdfShouldHaveImages(ctx context.Context, name string, images int) error { + path := fmt.Sprintf("%s/%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name) + + _, err := os.Stat(path) + if os.IsNotExist(err) { + return fmt.Errorf("PDF %q does not exist", path) + } + + cmd := []string{ + "pdfimages", + "-list", + filepath.Base(path), + } + + output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path) + if err != nil { + return fmt.Errorf("exec %q: %w", cmd, err) + } + + // pdfimages -list outputs a header (2 lines) then one line per image. + lines := strings.Split(strings.TrimSpace(output), "\n") + actual := 0 + if len(lines) > 2 { + actual = len(lines) - 2 + } + + if actual != images { + return fmt.Errorf("expected %d image(s), but actual is %d", images, actual) + } + + return nil +} + func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context, name string, kind string) error { var path string if !strings.HasPrefix(name, "*_") { @@ -1270,6 +1303,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation) ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage) + ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages) ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { if s.gotenbergContainer != nil { errTerminate := s.gotenbergContainer.Terminate(ctx, testcontainers.StopTimeout(0)) diff --git a/test/integration/testdata/html-with-asset/image.png b/test/integration/testdata/html-with-asset/image.png new file mode 100644 index 0000000000000000000000000000000000000000..62a5f8f47fec02344e5bf9061888262f677cf5d6 GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar*6yf1E$Sz`)GN$Z+!C Rr1wB^22WQ%mvv4FO#seq5RCu; literal 0 HcmV?d00001 diff --git a/test/integration/testdata/html-with-asset/index.html b/test/integration/testdata/html-with-asset/index.html new file mode 100644 index 0000000..0fc90bb --- /dev/null +++ b/test/integration/testdata/html-with-asset/index.html @@ -0,0 +1,11 @@ + + + + + HTML with Asset + + +

Asset test

+ Test image + +