Skip to content

Increase HTTP request timeout to 60 seconds - #1407

Merged
gcsecsey merged 3 commits into
trunkfrom
stu-323-feature-request-plugin-updater-setting-to-fix-timeout
May 20, 2025
Merged

Increase HTTP request timeout to 60 seconds#1407
gcsecsey merged 3 commits into
trunkfrom
stu-323-feature-request-plugin-updater-setting-to-fix-timeout

Conversation

@gcsecsey

@gcsecsey gcsecsey commented May 16, 2025

Copy link
Copy Markdown
Member

Related issues

Fixes STU-323 Feature Request: Plugin Updater Setting (to Fix Timeout)

Proposed Changes

This PR addresses timeout issues with plugin updates by increasing the default HTTP request timeout to 60 seconds.

  • Added a new MU plugin 0-http-request-timeout.php that sets the default HTTP request timeout and curl timeouts to 60 seconds

Testing Instructions

  1. Temporarily decrease the timeouts to 1 second:
--- a/vendor/wp-now/src/wp-now.ts
+++ b/vendor/wp-now/src/wp-now.ts
@@ -623,13 +623,13 @@ async function mountInternalMuPlugins( php: PHP, options: WPNowOptions ) {
                `<?php
                // Set the default HTTP request timeout to 60 seconds
                add_filter( 'http_request_timeout', function() {
-                       return 60;
+                       return 1;
                } );
 
                // Set the default cURL timeout to 60 second
                add_action('http_api_curl', function($curl, $url, $options) {
-                       curl_setopt( $curl, CURLOPT_CONNECTTIMEOUT, 60 );
-                       curl_setopt($curl, CURLOPT_TIMEOUT, 60);
+                       curl_setopt( $curl, CURLOPT_CONNECTTIMEOUT, 1 );
+                       curl_setopt($curl, CURLOPT_TIMEOUT, 1);
                        return $curl;
                }, 1, 3);
  1. Run npm start
  2. In one of your sites, open the functions.php of the active theme, and add this snippet:
add_action(
    'wp_head', function () {
        $response = wp_remote_get('https://httpstat.us/200?sleep=5000'); // 5-second delay

        if (is_wp_error($response) ) {
            echo '<div style="background: #fee; color: #a00; padding: 10px; font-weight: bold;">';
            echo 'Request failed: ' . esc_html($response->get_error_message());
            echo '</div>';
        } else {
            echo '<div style="background: #efe; color: #060; padding: 10px; font-weight: bold;">';
            echo 'Request succeeded: HTTP ' . esc_html(wp_remote_retrieve_response_code($response));
            echo '</div>';
        }
    }
);
  1. Go to the site's homepage and check that you receive a timeout error
  2. Remove the changes from wp-now.ts, and kill and restart Studio
  3. Go to the site's homepage and check that the request succeeds
with 1 second timeout with 60 seconds timeout
image image

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?
@gcsecsey
gcsecsey requested a review from a team May 16, 2025 16:34
@gcsecsey
gcsecsey marked this pull request as ready for review May 16, 2025 16:34
Comment thread vendor/wp-now/src/wp-now.ts Outdated
@gcsecsey
gcsecsey requested review from a team and nightnei May 19, 2025 11:47

@epeicher epeicher 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.

Thanks for the detailed test steps and the utility code for testing! I have tested it, and it works as advertised. I can see the success message after applying the changes.

Before After
CleanShot 2025-05-19 at 14 47 22@2x CleanShot 2025-05-19 at 14 52 40@2x

P.S. Please update the 5-second delay in the test instructions as it was adding 50 seconds delay! 😝

@gcsecsey

Copy link
Copy Markdown
Member Author

P.S. Please update the 5-second delay in the test instructions as it was adding 50 seconds delay! 😝

Thanks for patiently reviewing this @epeicher and sorry for the typo there. 😅

@gcsecsey
gcsecsey merged commit 69419a6 into trunk May 20, 2025
@gcsecsey
gcsecsey deleted the stu-323-feature-request-plugin-updater-setting-to-fix-timeout branch May 20, 2025 09:30
bcotrim pushed a commit that referenced this pull request May 27, 2025
* Increase HTTP request timeout to 60 seconds

* Increase curl timeout to 60 seconds

* update comments
bcotrim added a commit that referenced this pull request May 27, 2025
commit d31733b
Author: katinthehatsite <katerynakodonenko@gmail.com>
Date:   Tue May 27 09:12:55 2025 +0200

    Studio: Delete old proof-of-concept WP-CLI implementation (#1426)

    * Remove old scripts

    * Remove old cli.ts

    * Cleanup tests

    * Clean up terminal opening and feature flag

    * Cleanup assistant code

    * Clean shortcuts code

    * Cleanup feature flags

    * Cleanup feature flags

    * Cleanup shortcuts sessions

    * Removed unused feature flag for assistant

    * Assistant code block test fix

    * Adjust preload

    * Cleanup index file and tests

    * Refactor terminal usage

    * Add back the lock

    * Remove unintended change

    * Remove unnecesary type for warp

    * Use bundled Ids

    * Cleanup event parameter

    ---------

    Co-authored-by: Kateryna Kodonenko <kateryna@automattic.com>

commit 4fac446
Author: Volodymyr Makukha <nei.css@gmail.com>
Date:   Mon May 26 20:19:35 2025 +0100

    Update Ukrainian translations - May 26 (#1439)

commit c7536ef
Author: Antonio Sejas <antonio.sejas@automattic.com>
Date:   Mon May 26 18:34:28 2025 +0100

    Load LTR and RTL stylesheets conditionally (#1429)

    * Add a new component
    * Load @wordpress/components css conditionally based on RTL and LTR.
    * Load index.css a.k.a main_window.css after loading the WordPress stylesheet

commit c80b70e
Author: Bero <berislav.grgicak@gmail.com>
Date:   Mon May 26 19:20:42 2025 +0200

    Update Playground packages to 1.0.38 and remove the Symlink manager (#1425)

    * Update Playground packages to 1.0.38
    * Remove SymlinkManager
    * Activate Playground's followSymlinks feature
    * Remove @php-wasm/universal patch

commit 5bfe676
Author: Wojtek Naruniec <wojtek.naruniec@automattic.com>
Date:   Mon May 26 15:40:18 2025 +0200

    Sanitize regex in Win editor path (#1411)

    * Sanitize regex

    * Import only escapeRegExp function

commit 2cd3e44
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Mon May 26 13:18:02 2025 +0200

    Bump version from 1.5.2-beta4 to 1.5.2 (#1436)

commit f16865e
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Mon May 26 12:38:24 2025 +0200

    Add release notes for 1.5.2 (#1434)

    * Add release notes for 1.5.2

    * Reorder release notes

    * Update release notes order and formatting

    * Added latest UI fix to the release notes

    * Consolidate Studio CLI fixes into a single bullet point

commit f6e48d5
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Mon May 26 12:21:30 2025 +0200

    Add translations for 1.5.2 (#1433)

    * Add translations for 1.5.2

    * Add latest Polish and Spanish translations

    * Update spanish translations

    ---------

    Co-authored-by: Antonio Sejas <antonio@sejas.es>

commit ae1d163
Author: Antonio Sejas <antonio.sejas@automattic.com>
Date:   Mon May 26 10:58:22 2025 +0100

    Share same styles between preview and sync tabs (#1435)

commit d6a7905
Author: Roberto Aranda <roberto.aranda@automattic.com>
Date:   Fri May 23 15:23:14 2025 +0200

    Return original URL param when it cannot be parsed (#1420)

    * Return the passed URL parameter when it cannot be parsed instead of an empty string

commit 91d04be
Author: Antonio Sejas <antonio.sejas@automattic.com>
Date:   Fri May 23 14:22:34 2025 +0100

    Update localized docs links for Studio Sync  and Studio CLI (#1431)

    * Add translation link to new Spanish studio docs CLI
    * Add hash to Studio sync supported sites

commit 6213094
Author: katinthehatsite <katerynakodonenko@gmail.com>
Date:   Fri May 23 14:27:53 2025 +0200

    Add patch to fix the accessibility with modals (#1423)

    Co-authored-by: Kateryna Kodonenko <kateryna@automattic.com>

commit b2724c0
Author: Rahul Gavande <rahul.gavande@automattic.com>
Date:   Fri May 23 14:45:11 2025 +0530

    Use uri scheme to open Warp terminal app (#1428)

commit 43c831b
Author: Fredrik Rombach Ekelund <fredrik@f26d.dev>
Date:   Wed May 21 16:10:40 2025 +0200

    CLI: Set UTF8 encoding for output on Windows (#1424)

commit b1c0eeb
Author: Volodymyr Makukha <nei.css@gmail.com>
Date:   Wed May 21 10:34:47 2025 +0100

    Fix hiding plugins spinner (#1419)

commit 48988ed
Author: Fredrik Rombach Ekelund <fredrik@f26d.dev>
Date:   Wed May 21 10:31:33 2025 +0200

    Add migration to rename launch uniques stat (#1415)

    * Add migration to rename launch uniques stat

    * Fix CLI zod schema

commit f363144
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Tue May 20 18:24:00 2025 +0200

    Bump version to 1.5.2-beta4 (#1421)

commit 9768e67
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Tue May 20 15:29:33 2025 +0200

    Pressable Sync: Fix remaining Sync modal issues (#1416)

    * Use absolute path for `WordPressLogoCircle` import

    * Use separate `isDisabled` to makde WP logo gray

    * Remove trailing space in site selector helper text

    * Decrease font size of sync sites modal learn more link

commit 9fab398
Author: Ian G. Maia <iangmaia@users.noreply.github.com>
Date:   Tue May 20 13:15:23 2025 +0200

    [Tooling] Update code to DRY the Windows build PS1 (#1408)

    * Update code to DRY the Windows build PS1

    * Update case of `Exit` command

    * Use `windows` instead of just `win` to prefix windows-related scripts

    * Add $LastExitCode check after `npm run make`

commit e5d4006
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Tue May 20 12:01:21 2025 +0200

    Improve version comparison logic to handle prerelease transitions correctly (#1403)

commit f0a282b
Author: Rahul Gavande <rahul.gavande@automattic.com>
Date:   Tue May 20 15:15:25 2025 +0530

    HTTPS: Convert domain name Unicode characters to ASCII characters (#1400)

    * Convert domain name unicode chars to ASCII

    * Do not use punnycode domain name for paths

    * Remove unncessary punnycode domain name

commit 6cdc351
Author: Roberto Aranda <roberto.aranda@automattic.com>
Date:   Tue May 20 11:35:52 2025 +0200

    Sync: Update notification to include Hostname (#1412)

    * Push: Update notification to include Hostname

    * Pull: Update notification to include hostname

    * Add hints for translators

commit 69419a6
Author: Gergely Csécsey <gergely.csecsey@automattic.com>
Date:   Tue May 20 10:30:57 2025 +0100

    Increase HTTP request timeout to 60 seconds (#1407)

    * Increase HTTP request timeout to 60 seconds

    * Increase curl timeout to 60 seconds

    * update comments

commit 6bcbbff
Author: Antonio Sejas <antonio.sejas@automattic.com>
Date:   Tue May 20 09:36:18 2025 +0100

    Update connect site text button when connecting another site (#1413)

    * Adapt the "Connect Site" button when connecting multiple sites on Sync.

commit 60a9cab
Author: Antonio Sejas <antonio.sejas@automattic.com>
Date:   Tue May 20 09:10:37 2025 +0100

    Avoid orphan in sync title (#1414)

commit 3b103cc
Author: Antonio Sejas <antonio.sejas@automattic.com>
Date:   Mon May 19 10:39:12 2025 +0100

    Release 1.5.2-beta3 (#1410)

commit a109671
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Mon May 19 09:45:17 2025 +0200

    Sync: Update sync tab text and remove WordPress logo from title (#1405)

    * Update sync tab text and remove WordPress logo from title

    * Fix related test

    * Use comma

    * Update copy of the bullet points

    * Replace `WP.com` with `WordPress.com`.

    Co-authored-by: Antonio Sejas <antonio.sejas@automattic.com>

    * Replace WP.com with WordPress.com

    Co-authored-by: Antonio Sejas <antonio.sejas@automattic.com>

    * Delete unused WordPress short logo component

    ---------

    Co-authored-by: Antonio Sejas <antonio.sejas@automattic.com>

commit 716bc7a
Author: Ivan Ottinger <ivan.ottinger@automattic.com>
Date:   Fri May 16 16:15:16 2025 +0200

    Sync: Update Sync modal content (#1406)

    * Update WordPress logo component `viewBox`

    * Update Sync modal heading

    * Update text below Sync search field

    * Update Sync modal site logos

    * Replace hardcoded arrows with `ArrowIcon` component in sync sites modal links

    * Localize sync modal Read more link using `getLocalizedLink` utility

commit 11b4302
Author: Roberto Aranda <roberto.aranda@automattic.com>
Date:   Fri May 16 15:14:40 2025 +0200

    Push: Read and update the progress from the sync endpoint (#1389)

    * Calls the GET /sync/import using the importId to obtain the progress
    * Use the progress from the /sync/import endpoint to update the progress in the importing state
    * Splits the progress of the importing state between backup and import

commit 7423bdf
Author: katinthehatsite <katerynakodonenko@gmail.com>
Date:   Fri May 16 11:40:18 2025 +0200

    Studio: Fix no directory or file error on Windows (#1391)

    * Fix no directory or file error on Windows

    * Cleanup directories

    ---------

    Co-authored-by: Kateryna Kodonenko <kateryna@automattic.com>

commit 1661ac4
Author: Antonio Sejas <antonio.sejas@automattic.com>
Date:   Fri May 16 10:04:32 2025 +0100

    Force display of what's new for minor version (#1404)

    * Force what's new display for minor version
    * Skip test that compares the patch version
chubes4 added a commit to chubes4/studio that referenced this pull request Apr 16, 2026
…imeout)

Fixes Automattic#3118.

The mu-plugin injected into every Studio site by getStandardMuPlugins
sets CURLOPT_LOW_SPEED_LIMIT=1024 + CURLOPT_LOW_SPEED_TIME=30, aborting
any outbound HTTP request where transfer rate stays below 1 KB/s for
30 consecutive seconds.

These values were correct for the original motivation (plugin/theme
downloads, Automattic#1407/Automattic#1913) -- a dead mirror delivering zero bytes is a
broken connection, and aborting fast is the right behavior.

They are incorrect for AI API requests, now a common Studio use case
for plugin developers building on top of OpenAI, Anthropic, Google,
etc. Non-streaming wp_remote_post calls to LLM providers produce zero
bytes on the socket until the model emits its first token. On flagship
models with long prompts, that first token commonly takes 30 to 90
seconds to arrive. The watchdog fires before content is delivered and
aborts with cURL error 28 ("Operation too slow").

Raise CURLOPT_LOW_SPEED_TIME from 30 seconds to 120 seconds. All other
thresholds unchanged, including the 300-second http_request_timeout
outer cap. A working plugin download produces far more than 1 KB/s
throughout its lifetime, so the low-speed watchdog never triggers for
that use case -- the Automattic#1913 Jetpack-on-3G test passes identically.
Dead-connection detection still works; it takes 120 seconds to detect
instead of 30, which is a small DX regression for an uncommon failure
mode, and the 300-second outer cap still catches fully hung requests.

The only observable behavior change: AI API requests that would have
died at 30 seconds now complete normally (up to the 300-second outer
cap, which is already enough for all current flagship models).

Reproduced cURL 28 aborts from Data Machine's daily memory compaction
against api.openai.com with gpt-5.4 on a ~30 KB prompt. Built the CLI
locally, verified the compiled mu-plugin contains the new value via
PHP reflection on the runtime closure
(CURLOPT_LOW_SPEED_TIME = 120), re-ran the compaction task against
the same flagship model on the same site, and confirmed end-to-end
completion.

AI assistance: I used Claude Code to research the history of this
mu-plugin (tracing through Automattic#1407 and Automattic#1913), evaluate the side
effects of each possible adjustment (raising the time vs. lowering
the limit vs. an escape-hatch filter), and draft this commit message
and the accompanying PR body. The engineering decision to raise the
time threshold to 120 seconds (rather than add a filter or
discriminate by URL) was mine after reviewing the options. I
verified the fix end-to-end against a live Studio site and the
original reproduction.
chubes4 added a commit to chubes4/studio that referenced this pull request Apr 17, 2026
…imeout)

Fixes Automattic#3118.

The mu-plugin injected into every Studio site by getStandardMuPlugins
sets CURLOPT_LOW_SPEED_LIMIT=1024 + CURLOPT_LOW_SPEED_TIME=30, aborting
any outbound HTTP request where transfer rate stays below 1 KB/s for
30 consecutive seconds.

These values were correct for the original motivation (plugin/theme
downloads, Automattic#1407/Automattic#1913) -- a dead mirror delivering zero bytes is a
broken connection, and aborting fast is the right behavior.

They are incorrect for AI API requests, now a common Studio use case
for plugin developers building on top of OpenAI, Anthropic, Google,
etc. Non-streaming wp_remote_post calls to LLM providers produce zero
bytes on the socket until the model emits its first token. On flagship
models with long prompts, that first token commonly takes 30 to 90
seconds to arrive. The watchdog fires before content is delivered and
aborts with cURL error 28 ("Operation too slow").

Raise CURLOPT_LOW_SPEED_TIME from 30 seconds to 120 seconds. All other
thresholds unchanged, including the 300-second http_request_timeout
outer cap. A working plugin download produces far more than 1 KB/s
throughout its lifetime, so the low-speed watchdog never triggers for
that use case -- the Automattic#1913 Jetpack-on-3G test passes identically.
Dead-connection detection still works; it takes 120 seconds to detect
instead of 30, which is a small DX regression for an uncommon failure
mode, and the 300-second outer cap still catches fully hung requests.

The only observable behavior change: AI API requests that would have
died at 30 seconds now complete normally (up to the 300-second outer
cap, which is already enough for all current flagship models).

Reproduced cURL 28 aborts from Data Machine's daily memory compaction
against api.openai.com with gpt-5.4 on a ~30 KB prompt. Built the CLI
locally, verified the compiled mu-plugin contains the new value via
PHP reflection on the runtime closure
(CURLOPT_LOW_SPEED_TIME = 120), re-ran the compaction task against
the same flagship model on the same site, and confirmed end-to-end
completion.

AI assistance: I used Claude Code to research the history of this
mu-plugin (tracing through Automattic#1407 and Automattic#1913), evaluate the side
effects of each possible adjustment (raising the time vs. lowering
the limit vs. an escape-hatch filter), and draft this commit message
and the accompanying PR body. The engineering decision to raise the
time threshold to 120 seconds (rather than add a filter or
discriminate by URL) was mine after reviewing the options. I
verified the fix end-to-end against a live Studio site and the
original reproduction.
chubes4 added a commit to chubes4/studio that referenced this pull request Apr 20, 2026
…imeout)

Fixes Automattic#3118.

The mu-plugin injected into every Studio site by getStandardMuPlugins
sets CURLOPT_LOW_SPEED_LIMIT=1024 + CURLOPT_LOW_SPEED_TIME=30, aborting
any outbound HTTP request where transfer rate stays below 1 KB/s for
30 consecutive seconds.

These values were correct for the original motivation (plugin/theme
downloads, Automattic#1407/Automattic#1913) -- a dead mirror delivering zero bytes is a
broken connection, and aborting fast is the right behavior.

They are incorrect for AI API requests, now a common Studio use case
for plugin developers building on top of OpenAI, Anthropic, Google,
etc. Non-streaming wp_remote_post calls to LLM providers produce zero
bytes on the socket until the model emits its first token. On flagship
models with long prompts, that first token commonly takes 30 to 90
seconds to arrive. The watchdog fires before content is delivered and
aborts with cURL error 28 ("Operation too slow").

Raise CURLOPT_LOW_SPEED_TIME from 30 seconds to 120 seconds. All other
thresholds unchanged, including the 300-second http_request_timeout
outer cap. A working plugin download produces far more than 1 KB/s
throughout its lifetime, so the low-speed watchdog never triggers for
that use case -- the Automattic#1913 Jetpack-on-3G test passes identically.
Dead-connection detection still works; it takes 120 seconds to detect
instead of 30, which is a small DX regression for an uncommon failure
mode, and the 300-second outer cap still catches fully hung requests.

The only observable behavior change: AI API requests that would have
died at 30 seconds now complete normally (up to the 300-second outer
cap, which is already enough for all current flagship models).

Reproduced cURL 28 aborts from Data Machine's daily memory compaction
against api.openai.com with gpt-5.4 on a ~30 KB prompt. Built the CLI
locally, verified the compiled mu-plugin contains the new value via
PHP reflection on the runtime closure
(CURLOPT_LOW_SPEED_TIME = 120), re-ran the compaction task against
the same flagship model on the same site, and confirmed end-to-end
completion.

AI assistance: I used Claude Code to research the history of this
mu-plugin (tracing through Automattic#1407 and Automattic#1913), evaluate the side
effects of each possible adjustment (raising the time vs. lowering
the limit vs. an escape-hatch filter), and draft this commit message
and the accompanying PR body. The engineering decision to raise the
time threshold to 120 seconds (rather than add a filter or
discriminate by URL) was mine after reviewing the options. I
verified the fix end-to-end against a live Studio site and the
original reproduction.
chubes4 added a commit to chubes4/studio that referenced this pull request Apr 23, 2026
…imeout)

Fixes Automattic#3118.

The mu-plugin injected into every Studio site by getStandardMuPlugins
sets CURLOPT_LOW_SPEED_LIMIT=1024 + CURLOPT_LOW_SPEED_TIME=30, aborting
any outbound HTTP request where transfer rate stays below 1 KB/s for
30 consecutive seconds.

These values were correct for the original motivation (plugin/theme
downloads, Automattic#1407/Automattic#1913) -- a dead mirror delivering zero bytes is a
broken connection, and aborting fast is the right behavior.

They are incorrect for AI API requests, now a common Studio use case
for plugin developers building on top of OpenAI, Anthropic, Google,
etc. Non-streaming wp_remote_post calls to LLM providers produce zero
bytes on the socket until the model emits its first token. On flagship
models with long prompts, that first token commonly takes 30 to 90
seconds to arrive. The watchdog fires before content is delivered and
aborts with cURL error 28 ("Operation too slow").

Raise CURLOPT_LOW_SPEED_TIME from 30 seconds to 120 seconds. All other
thresholds unchanged, including the 300-second http_request_timeout
outer cap. A working plugin download produces far more than 1 KB/s
throughout its lifetime, so the low-speed watchdog never triggers for
that use case -- the Automattic#1913 Jetpack-on-3G test passes identically.
Dead-connection detection still works; it takes 120 seconds to detect
instead of 30, which is a small DX regression for an uncommon failure
mode, and the 300-second outer cap still catches fully hung requests.

The only observable behavior change: AI API requests that would have
died at 30 seconds now complete normally (up to the 300-second outer
cap, which is already enough for all current flagship models).

Reproduced cURL 28 aborts from Data Machine's daily memory compaction
against api.openai.com with gpt-5.4 on a ~30 KB prompt. Built the CLI
locally, verified the compiled mu-plugin contains the new value via
PHP reflection on the runtime closure
(CURLOPT_LOW_SPEED_TIME = 120), re-ran the compaction task against
the same flagship model on the same site, and confirmed end-to-end
completion.

AI assistance: I used Claude Code to research the history of this
mu-plugin (tracing through Automattic#1407 and Automattic#1913), evaluate the side
effects of each possible adjustment (raising the time vs. lowering
the limit vs. an escape-hatch filter), and draft this commit message
and the accompanying PR body. The engineering decision to raise the
time threshold to 120 seconds (rather than add a filter or
discriminate by URL) was mine after reviewing the options. I
verified the fix end-to-end against a live Studio site and the
original reproduction.
chubes4 added a commit to chubes4/studio that referenced this pull request May 29, 2026
…imeout)

Fixes Automattic#3118.

The mu-plugin injected into every Studio site by getStandardMuPlugins
sets CURLOPT_LOW_SPEED_LIMIT=1024 + CURLOPT_LOW_SPEED_TIME=30, aborting
any outbound HTTP request where transfer rate stays below 1 KB/s for
30 consecutive seconds.

These values were correct for the original motivation (plugin/theme
downloads, Automattic#1407/Automattic#1913) -- a dead mirror delivering zero bytes is a
broken connection, and aborting fast is the right behavior.

They are incorrect for AI API requests, now a common Studio use case
for plugin developers building on top of OpenAI, Anthropic, Google,
etc. Non-streaming wp_remote_post calls to LLM providers produce zero
bytes on the socket until the model emits its first token. On flagship
models with long prompts, that first token commonly takes 30 to 90
seconds to arrive. The watchdog fires before content is delivered and
aborts with cURL error 28 ("Operation too slow").

Raise CURLOPT_LOW_SPEED_TIME from 30 seconds to 120 seconds. All other
thresholds unchanged, including the 300-second http_request_timeout
outer cap. A working plugin download produces far more than 1 KB/s
throughout its lifetime, so the low-speed watchdog never triggers for
that use case -- the Automattic#1913 Jetpack-on-3G test passes identically.
Dead-connection detection still works; it takes 120 seconds to detect
instead of 30, which is a small DX regression for an uncommon failure
mode, and the 300-second outer cap still catches fully hung requests.

The only observable behavior change: AI API requests that would have
died at 30 seconds now complete normally (up to the 300-second outer
cap, which is already enough for all current flagship models).

Reproduced cURL 28 aborts from Data Machine's daily memory compaction
against api.openai.com with gpt-5.4 on a ~30 KB prompt. Built the CLI
locally, verified the compiled mu-plugin contains the new value via
PHP reflection on the runtime closure
(CURLOPT_LOW_SPEED_TIME = 120), re-ran the compaction task against
the same flagship model on the same site, and confirmed end-to-end
completion.

AI assistance: I used Claude Code to research the history of this
mu-plugin (tracing through Automattic#1407 and Automattic#1913), evaluate the side
effects of each possible adjustment (raising the time vs. lowering
the limit vs. an escape-hatch filter), and draft this commit message
and the accompanying PR body. The engineering decision to raise the
time threshold to 120 seconds (rather than add a filter or
discriminate by URL) was mine after reviewing the options. I
verified the fix end-to-end against a live Studio site and the
original reproduction.
fredrikekelund added a commit that referenced this pull request Jul 7, 2026
…imeout) (#3120)

## Related issues

Fixes #3118.

## Proposed Changes

- Raise `CURLOPT_LOW_SPEED_TIME` from 30 seconds to 120 seconds in
`getStandardMuPlugins`' `0-http-request-timeout.php`.
- Update the inline documentation to explain the new threshold and the
AI-API-request use case.
- No other thresholds changed: `CURLOPT_CONNECTTIMEOUT` stays at 30s,
`CURLOPT_LOW_SPEED_LIMIT` stays at 1024 B/s, `http_request_timeout`
stays at 300s.

## Background

The `0-http-request-timeout.php` mu-plugin was introduced in #1407 (May
2025, hard 60s timeout) and refined in #1913 (October 2025, replaced
hard timeout with progress-based watchdog). Both PRs were correct for
their motivation: plugin/theme downloads on slow connections. The #1913
design — abort if transfer stays below 1 KB/s for 30 consecutive seconds
— fails fast on dead mirrors while letting working slow downloads
(Jetpack 33 MB on 3G) complete.

Since October 2025, AI-powered WordPress plugins (Data Machine,
Intelligence, and others) have become a common Studio use case.
Non-streaming `wp_remote_post` calls to LLM providers produce zero bytes
on the socket until the model emits its first token. On flagship models
with long prompts, that first token commonly takes 30 to 90 seconds. The
watchdog fires before any content is delivered, aborting with cURL error
28.

See #3118 for the full reproduction and context.

## Side effects of the change

| Use case | Before (30s) | After (120s) |
|---|---|---|
| Plugin/theme download on fast internet | works | works |
| Plugin/theme download on slow internet (Jetpack-on-3G test from #1913)
| works (bytes flow >1 KB/s) | works (same) |
| Dead mirror (0 B/s forever) | aborts in 30s | aborts in 120s |
| Hung connection (bytes delivered, then stall) | aborts 30s after stall
| aborts 120s after stall |
| AI API request with <30s TTFB | works | works |
| **AI API request with 30-120s TTFB** | **aborts with cURL 28** |
**works** |
| AI API request with >120s TTFB | aborts | aborts via 300s
`http_request_timeout` outer cap |

The only observable behavior change: AI API requests that were dying now
complete. The only regression is that a truly hung connection takes 90
seconds longer to detect. The 300-second outer cap still catches fully
broken requests. This is a small DX trade-off for an uncommon failure
mode, in exchange for first-class AI API support.

No new escape-hatch filter, no URL-based discrimination, no new
configuration knob. The one default value moves to a number that makes
both use cases work.

## Testing Instructions

### Existing #1913 regression test (plugin download on throttled
connection)

Per Bernardo's original methodology:

1. Install Network Link Conditioner (from Apple's Additional Tools for
Xcode)
2. Set throttling to a slow profile (e.g., \"3G\" or \"Edge\")
3. In Studio, navigate to `/wp-admin/plugin-install.php`
4. Install the Jetpack plugin (33 MB)
5. Expected: completes successfully on throttled connection (may take
several minutes)

Should pass identically before and after this change — the
slow-but-working download produces far more than 1 KB/s throughout, so
the low-speed watchdog never triggers regardless of whether the
threshold is 30s or 120s.

### New test case (slow-TTFB AI API request)

1. Install a plugin that uses `wp_remote_post` to call
`https://api.openai.com/v1/chat/completions` (e.g., Data Machine or any
ai-http-client consumer).
2. Configure it to use a flagship OpenAI model (`gpt-5.4` or similar).
3. Send a non-streaming request with a large prompt (~20 KB system
prompt + long user message designed to trigger reasoning).
4. Expected before: fails with cURL error 28 (\"Operation too slow. Less
than 1024 bytes/sec transferred during the last 30 seconds\") on
requests where OpenAI's TTFB exceeds 30 seconds.
5. Expected after: completes normally for any TTFB up to 120 seconds;
fails via the 300s outer cap beyond that.

### Dead-connection regression test

1. `wp eval
'var_dump(wp_remote_get(\"https://example.invalid/never-responds\"));'`
2. Expected: times out in approximately 30s (connect phase — not
affected by this change).

For a case that reaches the low-speed watchdog specifically, you'd need
a server that accepts the connection but then sends zero bytes
indefinitely (uncommon). That case would now take 120s to detect instead
of 30s.

### Verifying the change is live in WASM

```php
// Run via wp eval on a Studio site after restarting it:
global $wp_filter;
foreach ( $wp_filter['http_api_curl']->callbacks as $prio => $cbs ) {
    foreach ( $cbs as $cb ) {
        if ( $cb['function'] instanceof Closure ) {
            $r = new ReflectionFunction( $cb['function'] );
            $f = $r->getFileName();
            $lines = file( $f );
            echo implode( '', array_slice( $lines, $r->getStartLine() - 1, $r->getEndLine() - $r->getStartLine() + 1 ) );
        }
    }
}
```

Expected output after restart should include `CURLOPT_LOW_SPEED_TIME,
120`.

## What this isn't

- **Not an escape-hatch filter.** Shouldn't require opt-in. Defaults
should just work for both use cases.
- **Not URL discrimination.** No hardcoded list of AI endpoints.
- **Not removing the mu-plugin.** #1913's protection is legitimate; this
tunes it.
- **Not touching the 300s outer cap.** Untouched. Separate concern if
ever raised.

## AI assistance disclosure

Per WordPress's [AI
Guidelines](https://make.wordpress.org/ai/handbook/ai-guidelines/),
disclosing meaningful AI assistance on this contribution:

- I used Claude Code to research the history of the mu-plugin (tracing
through #1407 and #1913), evaluate the side effects of possible
adjustments (raising the time vs. lowering the limit vs. an escape-hatch
filter), and draft the commit message and PR body.
- The engineering decision (raise time threshold to 120s rather than add
a filter or discriminate by URL) was mine after reviewing the options
with Claude Code.
- I tested the fix end-to-end on a live Studio site running the Data
Machine plugin, including verifying the compiled mu-plugin contains the
new value via PHP reflection on the runtime closure, and re-running the
daily memory compaction task against flagship gpt-5.4 to confirm
completion.
- The original cURL 28 reproduction that motivated this fix was observed
during real Data Machine usage earlier in the same session.

All substantive design decisions and the testing were my own; AI was
used for investigation, pattern recognition, and drafting.

## Pre-merge Checklist

- [x] Have you checked for TypeScript, React or other console errors?

Co-authored-by: Fredrik Rombach Ekelund <fredrik.rombach.ekelund@automattic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants