You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@stormcrawler.apache.org by rz...@apache.org on 2026/04/27 07:48:58 UTC

(stormcrawler) 01/01: feat(playwright): capture rendered DOM on non-2xx responses

This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch 1890
in repository https://gitbox.apache.org/repos/asf/stormcrawler.git

commit b595fb421c796f4e4ad20d2a19c1420a1bc6cc59
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Mon Apr 27 09:48:45 2026 +0200

    feat(playwright): capture rendered DOM on non-2xx responses
    
    Add two opt-in configuration flags to the Playwright protocol so that
    Single-Page Applications which return a non-2xx stub document and then
    hydrate via JavaScript can be crawled correctly:
    
    - playwright.capture.content.on.error: also capture page.content() when
      the origin response status is not 2xx (default false, preserves
      current behavior).
    - playwright.override.status.on.content: when content was captured for
      a non-2xx response, report status 200 to downstream components and
      preserve the original status in the response metadata under
      playwright.origin.status (default false).
    
    Document both flags in the module README and playwright-conf.yaml.
---
 external/playwright/README.md                      | 19 ++++++++++++++++
 external/playwright/playwright-conf.yaml           | 12 ++++++++++
 .../protocol/playwright/HttpProtocol.java          | 26 ++++++++++++++++++++--
 3 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/external/playwright/README.md b/external/playwright/README.md
index 4cbb1d13..725bea64 100644
--- a/external/playwright/README.md
+++ b/external/playwright/README.md
@@ -28,3 +28,22 @@ mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="inst
 
 The setting `playwright.skip.download` to `true` in the configuration will assume that the browser has been installed and will not trigger the installation of all the different browsers.
 
+## Configuration
+
+| Property | Default | Description |
+|---|---|---|
+| `playwright.cdp.url` | _unset_ | If set, connect to an existing Chrome instance via the Chrome DevTools Protocol (e.g. `http://localhost:9222`) instead of launching a browser. |
+| `playwright.remote.ws` | _unset_ | If set, connect to a remote Playwright server over WebSocket (e.g. `ws://localhost:3000/`). Mutually exclusive with `playwright.cdp.url`. |
+| `playwright.skip.download` | `false` | If `true`, sets `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=true` so Playwright will not install browsers. Implicitly forced to `true` when `playwright.cdp.url` or `playwright.remote.ws` is set. |
+| `playwright.load.event` | `load` | The Playwright `WaitUntilState` to wait for before considering the page ready. Accepts `load`, `domcontentloaded`, or `networkidle`. |
+| `playwright.skip.resource.types` | _empty_ | List of resource types to abort during navigation (`document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`). |
+| `playwright.evaluations` | _empty_ | List of JavaScript expressions evaluated on the page after load. Each result is JSON-serialized and stored in the response metadata under the expression itself as the key. |
+| `playwright.capture.content.on.error` | `false` | By default the rendered DOM is only captured when the origin returns a 2xx status. Set to `true` to also capture `page.content()` for non-2xx responses — useful for Single-Page Applications that return a non-2xx stub document and then hydrate the real content via JavaScript. |
+| `playwright.override.status.on.content` | `false` | When the rendered DOM was captured for a non-2xx response, override the reported HTTP status with `200` so downstream components treat the URL as `FETCHED`. The original origin status is preserved in the response metadata under the key `playwright.origin.status`. No-op unless `playwright.capture.content.on.error` is also `true`. |
+
+Per-URL metadata triggers:
+
+| Metadata key | Effect |
+|---|---|
+| `playwright.trace` | If present on the input metadata, a Playwright trace zip is recorded for the navigation and its path is returned in the response metadata under the same key. |
+
diff --git a/external/playwright/playwright-conf.yaml b/external/playwright/playwright-conf.yaml
index a6d16c61..1020537b 100644
--- a/external/playwright/playwright-conf.yaml
+++ b/external/playwright/playwright-conf.yaml
@@ -24,3 +24,15 @@ config:
   # com.microsoft.playwright.options.WaitUntilState
   # playwright.load.event: "domcontentloaded"
 
+  # By default the rendered DOM is only captured when the origin returns a 2xx
+  # status. Enable this to also capture page.content() for non-2xx responses
+  # (useful for SPAs that return e.g. a 404 stub and then hydrate via JS).
+  # playwright.capture.content.on.error: false
+
+  # When the rendered DOM was captured for a non-2xx response, override the
+  # reported HTTP status with 200 so downstream components treat the URL as
+  # FETCHED. The original origin status is preserved in the response metadata
+  # under the key "playwright.origin.status". No-op unless
+  # playwright.capture.content.on.error is also true.
+  # playwright.override.status.on.content: false
+
diff --git a/external/playwright/src/main/java/org/apache/stormcrawler/protocol/playwright/HttpProtocol.java b/external/playwright/src/main/java/org/apache/stormcrawler/protocol/playwright/HttpProtocol.java
index ef1f2148..b19e1701 100644
--- a/external/playwright/src/main/java/org/apache/stormcrawler/protocol/playwright/HttpProtocol.java
+++ b/external/playwright/src/main/java/org/apache/stormcrawler/protocol/playwright/HttpProtocol.java
@@ -62,6 +62,10 @@ public class HttpProtocol extends AbstractHttpProtocol {
 
     private int timeout = 10000;
 
+    private boolean captureContentOnError = false;
+
+    private boolean overrideStatusOnContent = false;
+
     private BrowserContext context;
 
     private Set<String> resourceTypesToSkip;
@@ -130,6 +134,12 @@ public class HttpProtocol extends AbstractHttpProtocol {
 
         timeout = ConfUtils.getInt(conf, "http.timeout", timeout);
 
+        captureContentOnError =
+                ConfUtils.getBoolean(conf, "playwright.capture.content.on.error", false);
+
+        overrideStatusOnContent =
+                ConfUtils.getBoolean(conf, "playwright.override.status.on.content", false);
+
         final String ua = getAgentString(conf);
 
         NewContextOptions b_c_options =
@@ -245,12 +255,24 @@ public class HttpProtocol extends AbstractHttpProtocol {
                                         responseMetaData.addValue(k, v);
                                     });
 
-                    if (Status.FETCHED == Status.fromHTTPCode(response.status())) {
+                    int httpStatus = response.status();
+                    boolean fetched = Status.FETCHED == Status.fromHTTPCode(httpStatus);
+                    boolean contentCaptured = false;
+
+                    if (fetched || captureContentOnError) {
                         // retrieve the rendered content
                         content = page.content().getBytes(StandardCharsets.UTF_8);
+                        contentCaptured = true;
                     }
 
-                    status.set(response.status());
+                    if (!fetched && contentCaptured && overrideStatusOnContent) {
+                        // expose the original origin status for diagnostics
+                        responseMetaData.setValue(
+                                "playwright.origin.status", Integer.toString(httpStatus));
+                        status.set(200);
+                    } else {
+                        status.set(httpStatus);
+                    }
 
                     // evaluate an expression and store the results
                     // in the metadata using the same string as key