Skip to content

fix: set $_SERVER variables: 'SCRIPT_NAME', 'PHP_SELF', and 'PATH_INFO' #2317

Open
henderkes wants to merge 10 commits intomainfrom
fix/script_name
Open

fix: set $_SERVER variables: 'SCRIPT_NAME', 'PHP_SELF', and 'PATH_INFO' #2317
henderkes wants to merge 10 commits intomainfrom
fix/script_name

Conversation

@henderkes
Copy link
Copy Markdown
Contributor

@henderkes henderkes commented Mar 27, 2026

fixes #2274 (comment)

apache/nginx/caddy pass PHP_SELF as SCRIPT_NAME + PATH_INFO, but our PATH_INFO wasn't working because our matcher stripped the rest of the path.

request url: localhost/index.php/en

# was non-worker:
SCRIPT_NAME: /index.php
PATH_INFO: 
PHP_SELF: /index.php
REQUEST_URL: /en

# was fastcgi:
SCRIPT_NAME: /index.php
PATH_INFO:  /en
PHP_SELF: /index.php/en
REQUEST_URL: /en

# was php_server worker
SCRIPT_NAME:
PATH_INFO:
PHP_SELF: /en
REQUEST_URL: /en

# now is always:
SCRIPT_NAME: /index.php
PATH_INFO: /en
PHP_SELF: /index.php/en
REQUEST_URL: /en

…rkers

apache passes PHP_SELF as SCRIPT_NAME + REQUEST_URL, but the docs say it's the same as SCRIPT_NAME and that's how Caddy+fpm behave too
@henderkes henderkes requested a review from AlliBalliBaba March 27, 2026 15:49
@henderkes henderkes marked this pull request as ready for review March 27, 2026 15:49
@henderkes henderkes marked this pull request as draft March 27, 2026 16:26
@henderkes henderkes changed the title fix $_SERVER['SCRIPT_NAME'] and $_SERVER['PHP_SELF'] in php_server workers fix $_SERVER['SCRIPT_NAME'], 'PHP_SELF', and 'PATH_INFO' Mar 27, 2026
@henderkes henderkes marked this pull request as ready for review March 27, 2026 17:05
cgi.go Outdated
// If a worker is already assigned explicitly, derive SCRIPT_NAME from its filename
if fc.worker != nil {
fc.scriptFilename = fc.worker.fileName
fc.scriptName = strings.TrimPrefix(fc.worker.fileName, fc.documentRoot)
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.

Hmm probably would be better to leave scriptName empty if the worker is in the public path:

root /some/path
worker /other/path {
  match *
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure about that. Shouldn't it show /other/path/index.php then?

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.

Not sure either TBH. In CGI spec it's the path relative to the root, so /other/path/index.php might be misleading.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Technically that's a path relative to the root, haha.

I don't see a better solution. Emptying it out would be even more of a violation.

cgi.go Outdated
// If a worker is already assigned explicitly, derive SCRIPT_NAME from its filename
if fc.worker != nil {
fc.scriptFilename = fc.worker.fileName
fc.scriptName = filepath.ToSlash(strings.TrimPrefix(fc.worker.fileName, fc.documentRoot))
Copy link
Copy Markdown
Contributor

@AlliBalliBaba AlliBalliBaba Mar 29, 2026

Choose a reason for hiding this comment

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

Can you also add a test for when the worker is outside of the public path (match)? Thinking about it again, scriptname should be empty since forwarding an absoule path when expecting a relative one is probably worse than forwarding nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Happy to add the test, but I'm not sure if I agree with passing nothing being the correct choice. We should probably agree on what the correct behaviour is first.

'SCRIPT_NAME'
Contains the current script's path. This is useful for pages which need to point to themselves. The FILE constant contains the full path and filename of the current (i.e. included) file.

If anything, "This is useful for pages which need to point to themselves." would mean that we should perhaps just give the current request uri. But on the other hand, "Contains the current script's path." would mean it should be the absolute path, rather than a uri.

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.

It's still expected to be a relative path, so I'd rather prevent potential redirects like this (and just leave it empty):

/path-the-user-visits -> /app/vendor/some-framework/worker.php

Realistically, most worker mode implementations probably just ignore SCRIPT_NAME.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't know, that doesn't feel right to me. I've changed it to what you suggested either way. Nobody should be using these server vars anyway.

@henderkes henderkes changed the title fix $_SERVER['SCRIPT_NAME'], 'PHP_SELF', and 'PATH_INFO' fix: set $_SERVER variables: 'SCRIPT_NAME', 'PHP_SELF', and 'PATH_INFO' Mar 31, 2026
@@ -208,17 +208,22 @@ func splitCgiPath(fc *frankenPHPContext) {
if splitPos := splitPos(path, splitPath); splitPos > -1 {
fc.docURI = path[:splitPos]
fc.pathInfo = path[splitPos:]
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.

Hmm I think if a worker is explicitly assigned, we should also ensure that the .php file is the actual worker file before determining pathInfo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In case of a match *?

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.

Yeah, we probably shouldn't allow pathname from a random script name like:

/random-script.php/pathname

Probably best to just leave it empty if script_name is empty.

It's a bit confusing because in all other cases the script name is guaranteed to be there because of rewrites.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

went for a simple if check in order to not introduce runtime another check
it's probably very unlikely to to a different worker file in the public document root

Copy link
Copy Markdown
Member

@dunglas dunglas left a comment

Choose a reason for hiding this comment

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

LGTM, but I didn't carefully review the CGI spec nor existing Apache/FPM behaviors. I trust you on this

run: go build
- name: Compile library tests
run: go test -race -v -x -c
run: go test -race -failfast -v -x -c
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer continuing to run the full test suite to catch all errors in one error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is there a way to see what actually failed without scrolling through 1000 lines? It irks me every time.

ignore_user_abort(true);

$handler = static function() {
echo "SCRIPT_NAME: " . ($_SERVER['SCRIPT_NAME'] ?? '(not set)') . "<br>";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: you could ass \n and the end of each line, so you could use backticks for better readability in tests

@@ -1,6 +1,8 @@
/caddy/frankenphp/Build
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes to revert?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added these because I often times find myself verifying bug reports there

I'm thinking they're fine in the .gitignore, but if you want I can revert

Copy link
Copy Markdown
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 fixes inconsistencies in $_SERVER CGI variables (notably SCRIPT_NAME, PHP_SELF, and PATH_INFO) in worker mode by ensuring path splitting happens consistently and by preserving “remainder” path segments through the Caddy rewrite/matching pipeline.

Changes:

  • Always split CGI path variables during request context creation, including in worker mode.
  • Set PHP_SELF from SCRIPT_NAME + PATH_INFO, and update CGI path splitting logic for worker-assigned requests.
  • Update Caddy php_server rewrite/match patterns to preserve remainder path info and add regression tests for server globals.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
context.go Always calls splitCgiPath(fc) to compute CGI path vars consistently.
cgi.go Updates PHP_SELF derivation and refactors splitCgiPath to better support worker mode.
caddy/php-server.go Preserves remainder in rewrite and matches *.php/* paths to support PATH_INFO.
caddy/module.go Same Caddy rewrite/matcher adjustments for the directive parser output.
caddy/caddy_test.go Adds coverage asserting expected globals for both worker and non-worker php_server setups.
testdata/server-globals.php New test helper script that prints key $_SERVER variables.
.github/workflows/tests.yaml Adds -failfast to speed up CI failure feedback.
.gitignore Ignores additional generated Caddy test artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
`, "caddyfile")

// Request to /en: no matching file, falls through to server-globals.php worker
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This comment is misleading: there is no worker configured in this test, so the request to /en is handled by the php_server index fallback, not a worker. Updating the comment would avoid confusion when maintaining these globals expectations.

Suggested change
// Request to /en: no matching file, falls through to server-globals.php worker
// Request to /en: no matching file, so php_server falls back to the index script server-globals.php

Copilot uses AI. Check for mistakes.
)

// === Site 2: php_server with its own worker ===
// because we specify a php file, PATH_INFO should be /en
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The comment says "PATH_INFO should be /en" for the /en request, but the assertion expects PATH_INFO to be empty. Either the comment or the expected output should be corrected so they agree.

Suggested change
// because we specify a php file, PATH_INFO should be /en
// because the request does not specify a php file, PATH_INFO should be empty

Copilot uses AI. Check for mistakes.

func TestPHPServerGlobals(t *testing.T) {
documentRoot, _ := filepath.Abs("../testdata")
scriptFilename := documentRoot + string(filepath.Separator) + "server-globals.php"
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

For portability/readability, build scriptFilename with filepath.Join(documentRoot, "server-globals.php") instead of string concatenation with filepath.Separator.

Suggested change
scriptFilename := documentRoot + string(filepath.Separator) + "server-globals.php"
scriptFilename := filepath.Join(documentRoot, "server-globals.php")

Copilot uses AI. Check for mistakes.
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.

$_SERVER['SCRIPT_NAME'] is empty in worker mode

4 participants