Skip to content

Conversation

@jasonbahl
Copy link
Collaborator

🐛 Problem

When plugins call wp_print_inline_script_tag() or similar output functions during the wp_enqueue_scripts action, it causes HTML to be output directly into the response stream. This breaks GraphQL JSON responses because HTML markup gets mixed with JSON, causing parsing errors on the client side.

Root Cause

WPGraphQL calls do_action( 'wp_enqueue_scripts' ) in multiple places to determine which scripts/styles are enqueued for specific nodes:

  • When querying enqueuedScripts or enqueuedStylesheets fields on posts, pages, terms, users, etc.
  • When querying registeredScripts or registeredStylesheets from the root query

If any plugin outputs HTML during this action (which is not uncommon), it corrupts the JSON response.

Example Error

{
  "errors": [
    {
      "message": "Unexpected token '<', \"\t<script>\nw\"... is not valid JSON",
      "stack": "SyntaxError: Unexpected token '<', \"\t<script>\nw\"... is not valid JSON"
    }
  ]
}

🔧 Solution

This PR implements Router-level output buffering during GraphQL HTTP request execution.

Advantages of Router-Level Fix

  • Single Point of Control: All GraphQL HTTP requests are protected with one fix
  • Comprehensive Coverage: Catches output from ANY source during GraphQL execution
  • Future-Proof: Automatically handles new sources of unwanted output
  • Clean Architecture: Router is responsible for HTTP response integrity
  • Better Performance: Single output buffer vs. multiple nested buffers

🔧 Implementation Details

The fix wraps the entire GraphQL execution in output buffering:

try {
    // Start output buffering to prevent unwanted output from breaking JSON response
    ob_start();
    
    $response = self::$request->execute_http();
    
    // Discard any captured output that could break the JSON response
    ob_end_clean();
    
    // ... continue processing
} catch ( \Throwable $error ) {
    // Ensure cleanup even on exceptions
    if ( ob_get_level() > 0 ) {
        ob_end_clean();
    }
    // ... existing error handling
}

🧪 Testing

Test Case Added

  • testPrintInlineScriptDoesNotBreakEnqueuedScriptsWithFix() verifies that calling wp_print_inline_script_tag() during wp_enqueue_scripts don't break GraphQL responses

Test Results

  • ✅ HTML output is captured and discarded
  • ✅ GraphQL response remains valid JSON
  • ✅ Enqueued scripts are properly returned
  • ✅ No errors in GraphQL response

Manual Testing

The fix was tested with the exact scenario from the issue:

add_action( 'wp_enqueue_scripts', function () {
    wp_print_inline_script_tag( 'window.Whoops = 1;' );
} );

Before fix: GraphQL response broken with HTML mixed in JSON
After fix: Clean JSON response with proper enqueued scripts data

📁 Files Changed

  • src/Router.php - Added comprehensive output buffering around GraphQL execution
  • tests/wpunit/EnqueuedScriptsTest.php - Added regression test

🔄 Backward Compatibility

This change is fully backward compatible:

  • No breaking changes to existing APIs
  • No changes to GraphQL schema
  • Only affects HTTP GraphQL requests (not internal graphql() calls)
  • Transparent to end users and developers

🎯 Impact

This fix protects against:

NOTE: I can't point to specific issues, but I've helped folks debug issues in the past where responses to certain queries would break the response in the IDE and we were never able to holistically determine a solution. I have a hunch that this will solve a lot of edge cases that were previously difficult to reproduce consistently.

🚀 Alternative Approaches Considered

  1. Resolver/Model level fixes - Would require adding output buffering to multiple individual resolvers

I initially explored adding output buffering to the Post, Term, User, etc Model where they call the wp_enqueue_scripts action. However, this would lead to:

  • ❌ Incomplete coverage
  • ❌ Higher maintenance burden
  • ❌ Easy to miss new cases
  1. Router-level fix (chosen approach)

Instead of duplicating the model-level output buffering effort, I implemented it in the Router which covers these cases and likely other misc edge cases as noted above.

  • ✅ Comprehensive coverage
  • ✅ Single point of control
  • ✅ Future-proof
  • ✅ Clean architecture

…eaking GraphQL responses

Fixes wp-graphql#3397 where plugins calling wp_print_inline_script_tag() or similar output
functions during the 'wp_enqueue_scripts' action would break GraphQL JSON responses
by mixing HTML with JSON output.

The issue occurred when:
1. A GraphQL query includes 'enqueuedScripts' or 'enqueuedStylesheets' fields
2. WPGraphQL fires the 'wp_enqueue_scripts' action to determine enqueued assets
3. Plugins output HTML during this action (e.g., wp_print_inline_script_tag())
4. The HTML output corrupts the JSON response, causing parsing errors

This fix implements comprehensive output buffering at the Router level during
GraphQL HTTP request execution. This approach:

- Captures and discards any unwanted output during GraphQL execution
- Provides comprehensive protection beyond just wp_enqueue_scripts
- Uses a single point of control for cleaner architecture
- Automatically handles future sources of unwanted output
- Includes proper cleanup in exception handling

I initally explored output buffering at the Model layer (i.e. Post.php, etc near the do_action('wp_enqueue_script'); calls, but had an epiphany that the Router-level approach would help fix other bugs like this that have been reported (but were always tricky to track down) in the past. This router approach is superior to resolver-level fixes as it protects the entire GraphQL execution context rather than individual action calls. 👏

Changes:
- Add output buffering around GraphQL execution in Router::process_http_request()
- Add test case to prevent regression
- Ensure proper cleanup in error conditions
@coveralls
Copy link

coveralls commented Sep 4, 2025

Coverage Status

coverage: 84.575% (-0.02%) from 84.593%
when pulling 9755082 on jasonbahl:fix/3397-enqueued-scripts-breaks-response-if-script-is-printed
into 9975538 on wp-graphql:develop.

@jasonbahl jasonbahl changed the title fix: Prevent HTML output from breaking GraphQL responses fix: Prevent printed scripts from breaking GraphQL responses Sep 4, 2025
…tput buffering

Adds testContentRenderingWorksWithRouterOutputBuffering() to ensure that the
Router-level output buffering fix for wp-graphql#3397 does not interfere with legitimate
content rendering that uses WordPress filters like 'the_content'.

The test verifies that:
- Content filters still work correctly (apply_filters returns processed content)
- GraphQL responses remain valid with Router-level output buffering
- HTML content from filters appears properly in the response

This test complements the existing fix validation and ensures no regression
in content rendering functionality.
…-enqueued-scripts-breaks-response-if-script-is-printed
@jasonbahl jasonbahl requested a review from Copilot September 4, 2025 16:14

This comment was marked as outdated.

@jasonbahl jasonbahl requested a review from Copilot September 4, 2025 18:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements router-level output buffering to prevent HTML output from corrupting GraphQL JSON responses. The fix addresses issues where plugins calling functions like wp_print_inline_script_tag() during GraphQL execution would output HTML directly into the response stream, breaking JSON parsing on the client side.

  • Router-level output buffering wrapped around the entire GraphQL HTTP execution
  • Comprehensive test coverage for various sources of unwanted HTML output during GraphQL execution
  • Protection against HTML output from WordPress hooks, WPGraphQL-specific hooks, and plugin behaviors

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/Router.php Added output buffering around GraphQL HTTP request execution with proper exception handling
tests/wpunit/EnqueuedScriptsTest.php Added regression test for the original issue with wp_print_inline_script_tag during wp_enqueue_scripts
tests/wpunit/RouterTest.php Added extensive test coverage for various scenarios where HTML output could break GraphQL responses

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@jasonbahl jasonbahl merged commit 071400c into wp-graphql:develop Sep 5, 2025
35 of 37 checks passed
pull bot pushed a commit to Zezo-Ai/wp-graphql that referenced this pull request Sep 5, 2025
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.

Including enqueuedScripts field can lead to broken output if scripts are printed in this action

2 participants