From a1f231a6d686495e18a9af1946aaff4914582a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 9 Jun 2026 17:20:40 +0200 Subject: [PATCH 1/3] feat: Add frankenphp worker support for more endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- Caddyfile | 24 ++++++++++ index.php | 39 +++-------------- lib/OC.php | 36 +++++++++++++++ ocs/v1.php | 108 +++++++++++++++++++++++---------------------- remote.php | 126 +++++++++++++++++++++++++++++------------------------ 5 files changed, 190 insertions(+), 143 deletions(-) diff --git a/Caddyfile b/Caddyfile index 032e5d32529c1..7472251129e62 100644 --- a/Caddyfile +++ b/Caddyfile @@ -3,11 +3,35 @@ # # THIS IS AN EXPERIMENTAL FEATURE # DO NOT USE THIS IN PRODUCTION, YOU HAVE BEEN WARNED. +{ + metrics + frankenphp { + num_threads 192 + max_threads 256 + # max_requests 500 + } +} localhost { php_server { worker { file index.php + num 32 + watch + } + worker { + file remote.php + num 32 + watch + } + worker { + file ocs/v1.php + num 32 + watch + } + worker { + file ocs/v2.php + num 32 watch } } diff --git a/index.php b/index.php index d4bdd263f79f3..69e30049db688 100644 --- a/index.php +++ b/index.php @@ -10,7 +10,6 @@ require_once __DIR__ . '/lib/versioncheck.php'; -use OC\Files\Filesystem; use OC\ServiceUnavailableException; use OC\User\LoginException; use OCP\HintException; @@ -24,23 +23,11 @@ \OC::boot(); -function resetStaticProperties(): void { - // FIXME needed because these use a static var - \OC_Hook::clear(); - \OC_Util::$styles = []; - \OC_Util::$headers = []; - \OC_User::setIncognitoMode(false); - \OC_User::$_setupedBackends = []; - \OC_App::reset(); - \OC_Helper::reset(); - Filesystem::reset(); -} - -$handler = static function () { +\OC::handleRequests(static function () { try { - resetStaticProperties(); - OC::init(); - OC::handleRequest(); + \OC::resetStaticProperties(); + \OC::init(); + \OC::handleRequest(); } catch (ServiceUnavailableException $ex) { Server::get(LoggerInterface::class)->error($ex->getMessage(), [ 'app' => 'index', @@ -124,20 +111,4 @@ function resetStaticProperties(): void { } Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); } -}; - -if (function_exists('frankenphp_handle_request') && isset($_SERVER['FRANKENPHP_WORKER']) && $_SERVER['FRANKENPHP_WORKER'] === '1') { - $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); - for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { - $keepRunning = \frankenphp_handle_request($handler); - - // Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation - gc_collect_cycles(); - - if (!$keepRunning) { - break; - } - } -} else { - $handler(); -} +}); diff --git a/lib/OC.php b/lib/OC.php index c7072df64b88d..c9ffcf7146152 100644 --- a/lib/OC.php +++ b/lib/OC.php @@ -1324,4 +1324,40 @@ protected static function tryAppAPILogin(OCP\IRequest $request): bool { return false; } } + + /** + * @internal + */ + public static function resetStaticProperties(): void { + // FIXME needed because these use a static var + \OC_Hook::clear(); + \OC_Util::$styles = []; + \OC_Util::$headers = []; + \OC_User::setIncognitoMode(false); + \OC_User::$_setupedBackends = []; + \OC_App::reset(); + \OC_Helper::reset(); + Filesystem::reset(); + } + + /** + * @internal + */ + public static function handleRequests(callable $handler): void { + if (function_exists('frankenphp_handle_request') && isset($_SERVER['FRANKENPHP_WORKER']) && $_SERVER['FRANKENPHP_WORKER'] === '1') { + $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); + for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { + $keepRunning = \frankenphp_handle_request($handler); + + // Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation + gc_collect_cycles(); + + if (!$keepRunning) { + break; + } + } + } else { + $handler(); + } + } } diff --git a/ocs/v1.php b/ocs/v1.php index 5c4d125f9eb84..03a79b3587a08 100644 --- a/ocs/v1.php +++ b/ocs/v1.php @@ -8,9 +8,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -require_once __DIR__ . '/../lib/versioncheck.php'; -require_once __DIR__ . '/../lib/base.php'; - use OC\OCS\ApiHelper; use OC\Route\Router; use OC\SystemConfig; @@ -28,60 +25,69 @@ use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; -$request = Server::get(IRequest::class); +require_once __DIR__ . '/../lib/versioncheck.php'; +require_once __DIR__ . '/../lib/OC.php'; -if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') { - // since the behavior of apps or remotes are unpredictable during - // an upgrade, return a 503 directly - ApiHelper::respond(503, 'Service unavailable', ['X-Nextcloud-Maintenance-Mode' => '1'], 503); - exit; -} +\OC::boot(); -/* - * Try the appframework routes - */ -try { - $appManager = Server::get(IAppManager::class); - $appManager->loadApps(['session']); - $appManager->loadApps(['authentication']); - $appManager->loadApps(['extended_authentication']); +\OC::handleRequests(static function () { + \OC::resetStaticProperties(); + \OC::init(); + $request = Server::get(IRequest::class); + + if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') { + // since the behavior of apps or remotes are unpredictable during + // an upgrade, return a 503 directly + ApiHelper::respond(503, 'Service unavailable', ['X-Nextcloud-Maintenance-Mode' => '1'], 503); + exit; + } + + /* + * Try the appframework routes + */ + try { + $appManager = Server::get(IAppManager::class); + $appManager->loadApps(['session']); + $appManager->loadApps(['authentication']); + $appManager->loadApps(['extended_authentication']); - $request->throwDecodingExceptionIfAny(); + $request->throwDecodingExceptionIfAny(); - if ($request->getPathInfo() !== '/core/update') { - // load all apps to get all api routes properly setup - // FIXME: this should ideally appear after handleLogin but will cause - // side effects in existing apps - $appManager->loadApps(); - if (!Server::get(IUserSession::class)->isLoggedIn()) { - OC::handleLogin($request); + if ($request->getPathInfo() !== '/core/update') { + // load all apps to get all api routes properly setup + // FIXME: this should ideally appear after handleLogin but will cause + // side effects in existing apps + $appManager->loadApps(); + if (!Server::get(IUserSession::class)->isLoggedIn()) { + OC::handleLogin($request); + } + } else { + $appManager->loadApps(['core']); } - } else { - $appManager->loadApps(['core']); - } - Server::get(Router::class)->match('/ocsapp' . $request->getRawPathInfo()); -} catch (MaxDelayReached $ex) { - ApiHelper::respond(Http::STATUS_TOO_MANY_REQUESTS, $ex->getMessage()); -} catch (ResourceNotFoundException $e) { - $txt = 'Invalid query, please check the syntax. API specifications are here:' - . ' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services.' . "\n"; - ApiHelper::respond(OCSController::RESPOND_NOT_FOUND, $txt); -} catch (MethodNotAllowedException $e) { - ApiHelper::setContentType(); - http_response_code(405); -} catch (LoginException $e) { - ApiHelper::respond(OCSController::RESPOND_UNAUTHORISED, 'Unauthorised'); -} catch (\Exception $e) { - Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); + Server::get(Router::class)->match('/ocsapp' . $request->getRawPathInfo()); + } catch (MaxDelayReached $ex) { + ApiHelper::respond(Http::STATUS_TOO_MANY_REQUESTS, $ex->getMessage()); + } catch (ResourceNotFoundException $e) { + $txt = 'Invalid query, please check the syntax. API specifications are here:' + . ' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services.' . "\n"; + ApiHelper::respond(OCSController::RESPOND_NOT_FOUND, $txt); + } catch (MethodNotAllowedException $e) { + ApiHelper::setContentType(); + http_response_code(405); + } catch (LoginException $e) { + ApiHelper::respond(OCSController::RESPOND_UNAUTHORISED, 'Unauthorised'); + } catch (\Exception $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); - $txt = 'Internal Server Error' . "\n"; - try { - if (Server::get(SystemConfig::class)->getValue('debug', false)) { - $txt .= $e->getMessage(); + $txt = 'Internal Server Error' . "\n"; + try { + if (Server::get(SystemConfig::class)->getValue('debug', false)) { + $txt .= $e->getMessage(); + } + } catch (\Throwable $e) { + // Just to be save } - } catch (\Throwable $e) { - // Just to be save + ApiHelper::respond(OCSController::RESPOND_SERVER_ERROR, $txt); } - ApiHelper::respond(OCSController::RESPOND_SERVER_ERROR, $txt); -} +}); diff --git a/remote.php b/remote.php index 27fb703c87ce2..2e0d0d6970610 100644 --- a/remote.php +++ b/remote.php @@ -1,24 +1,27 @@ getAppValue('core', 'remote_' . $service); } -try { - require_once __DIR__ . '/lib/base.php'; +require_once __DIR__ . '/lib/OC.php'; - // All resources served via the DAV endpoint should have the strictest possible - // policy. Exempted from this is the SabreDAV browser plugin which overwrites - // this policy with a softer one if debug mode is enabled. - header("Content-Security-Policy: default-src 'none';"); +\OC::boot(); - if (Util::needUpgrade()) { - // since the behavior of apps or remotes are unpredictable during - // an upgrade, return a 503 directly - throw new RemoteException('Service unavailable', 503); - } +\OC::handleRequests(static function () { + try { + \OC::resetStaticProperties(); + \OC::init(); + + // All resources served via the DAV endpoint should have the strictest possible + // policy. Exempted from this is the SabreDAV browser plugin which overwrites + // this policy with a softer one if debug mode is enabled. + header("Content-Security-Policy: default-src 'none';"); + + if (Util::needUpgrade()) { + // since the behavior of apps or remotes are unpredictable during + // an upgrade, return a 503 directly + throw new RemoteException('Service unavailable', 503); + } - $request = \OCP\Server::get(IRequest::class); - $pathInfo = $request->getPathInfo(); - if ($pathInfo === false || $pathInfo === '') { - throw new RemoteException('Path not found', 404); - } - if (!$pos = strpos($pathInfo, '/', 1)) { - $pos = strlen($pathInfo); - } - $service = substr($pathInfo, 1, $pos - 1); + $request = \OCP\Server::get(IRequest::class); + $pathInfo = $request->getPathInfo(); + if ($pathInfo === false || $pathInfo === '') { + throw new RemoteException('Path not found', 404); + } + if (!$pos = strpos($pathInfo, '/', 1)) { + $pos = strlen($pathInfo); + } + $service = substr($pathInfo, 1, $pos - 1); - $file = resolveService($service); + $file = resolveService($service); - if (is_null($file)) { - throw new RemoteException('Path not found', 404); - } + if (is_null($file)) { + throw new RemoteException('Path not found', 404); + } - $file = ltrim($file, '/'); - - $parts = explode('/', $file, 2); - $app = $parts[0]; - - // Load all required applications - \OC::$REQUESTEDAPP = $app; - $appManager = \OCP\Server::get(IAppManager::class); - $appManager->loadApps(['authentication']); - $appManager->loadApps(['extended_authentication']); - $appManager->loadApps(['filesystem', 'logging']); - - switch ($app) { - case 'core': - $file = OC::$SERVERROOT . '/' . $file; - break; - default: - if (!$appManager->isEnabledForUser($app)) { - throw new RemoteException('App not installed: ' . $app); - } - $appManager->loadApp($app); - $file = $appManager->getAppPath($app) . '/' . ($parts[1] ?? ''); - break; + $file = ltrim($file, '/'); + + $parts = explode('/', $file, 2); + $app = $parts[0]; + + // Load all required applications + \OC::$REQUESTEDAPP = $app; + $appManager = \OCP\Server::get(IAppManager::class); + $appManager->loadApps(['authentication']); + $appManager->loadApps(['extended_authentication']); + $appManager->loadApps(['filesystem', 'logging']); + + switch ($app) { + case 'core': + $file = OC::$SERVERROOT . '/' . $file; + break; + default: + if (!$appManager->isEnabledForUser($app)) { + throw new RemoteException('App not installed: ' . $app); + } + $appManager->loadApp($app); + $file = $appManager->getAppPath($app) . '/' . ($parts[1] ?? ''); + break; + } + $baseuri = OC::$WEBROOT . '/remote.php/' . $service . '/'; + require_once $file; + } catch (Exception $ex) { + handleException($ex); + } catch (Error $e) { + handleException($e); } - $baseuri = OC::$WEBROOT . '/remote.php/' . $service . '/'; - require_once $file; -} catch (Exception $ex) { - handleException($ex); -} catch (Error $e) { - handleException($e); -} +}); From dd5c00d27d0eb587dc42ff26706c4bc4c824dff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 9 Jun 2026 18:05:19 +0200 Subject: [PATCH 2/3] fixup! feat: Add frankenphp worker support for more endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/OC.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/OC.php b/lib/OC.php index c9ffcf7146152..ba8c5b78d1ac9 100644 --- a/lib/OC.php +++ b/lib/OC.php @@ -7,6 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +use OC\Files\Filesystem; use OC\Profiler\BuiltInProfiler; use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Share20\GroupDeletedListener; From ef7492368e6723d7a9cee384bd0f23d3b25ad4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 9 Jun 2026 18:10:18 +0200 Subject: [PATCH 3/3] chore: Use a better naming for init function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- index.php | 3 +-- lib/OC.php | 12 ++++++++++-- lib/base.php | 2 +- ocs/v1.php | 3 +-- remote.php | 3 +-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/index.php b/index.php index 69e30049db688..192d72327f251 100644 --- a/index.php +++ b/index.php @@ -25,8 +25,7 @@ \OC::handleRequests(static function () { try { - \OC::resetStaticProperties(); - \OC::init(); + \OC::initForRequest(); \OC::handleRequest(); } catch (ServiceUnavailableException $ex) { Server::get(LoggerInterface::class)->error($ex->getMessage(), [ diff --git a/lib/OC.php b/lib/OC.php index ba8c5b78d1ac9..b2aebd9740bd2 100644 --- a/lib/OC.php +++ b/lib/OC.php @@ -666,6 +666,9 @@ private static function addSecurityHeaders(): void { } } + /* + * Called only once at the beginning to setup things + */ public static function boot(): void { // prevent any XML processing from loading external entities libxml_set_external_entity_loader(static function () { @@ -726,7 +729,12 @@ public static function boot(): void { } } - public static function init(): void { + /* + * Called before each request served if the same worker serves several request + */ + public static function initForRequest(): void { + self::resetStaticProperties(); + // First handle PHP configuration and copy auth headers to the expected // $_SERVER variable before doing anything Server object related self::setRequiredIniValues(); @@ -1329,7 +1337,7 @@ protected static function tryAppAPILogin(OCP\IRequest $request): bool { /** * @internal */ - public static function resetStaticProperties(): void { + private static function resetStaticProperties(): void { // FIXME needed because these use a static var \OC_Hook::clear(); \OC_Util::$styles = []; diff --git a/lib/base.php b/lib/base.php index d38e1fe259080..7e098be7062c0 100644 --- a/lib/base.php +++ b/lib/base.php @@ -9,4 +9,4 @@ require_once __DIR__ . '/OC.php'; \OC::boot(); -\OC::init(); +\OC::initForRequest(); diff --git a/ocs/v1.php b/ocs/v1.php index 03a79b3587a08..0a7920fba04f9 100644 --- a/ocs/v1.php +++ b/ocs/v1.php @@ -31,8 +31,7 @@ \OC::boot(); \OC::handleRequests(static function () { - \OC::resetStaticProperties(); - \OC::init(); + \OC::initForRequest(); $request = Server::get(IRequest::class); if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') { diff --git a/remote.php b/remote.php index 2e0d0d6970610..3bfa67e9ec09d 100644 --- a/remote.php +++ b/remote.php @@ -103,8 +103,7 @@ function resolveService($service) { \OC::handleRequests(static function () { try { - \OC::resetStaticProperties(); - \OC::init(); + \OC::initForRequest(); // All resources served via the DAV endpoint should have the strictest possible // policy. Exempted from this is the SabreDAV browser plugin which overwrites