Skip to content

Commit 1c5c853

Browse files
committed
Show actual PHP error details for 500-level responses
Fixes #5
1 parent e15a2ed commit 1c5c853

File tree

4 files changed

+106
-44
lines changed

4 files changed

+106
-44
lines changed

index.js

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,67 @@
11
import process from 'node:process';
22
import path from 'node:path';
33
import {spawn} from 'node:child_process';
4-
import http from 'node:http';
4+
import {setTimeout} from 'node:timers/promises';
55
import open from 'open';
66
import binaryVersionCheck from 'binary-version-check';
77
import getPort from 'get-port';
88

99
const isServerRunning = (hostname, port, pathname) => new Promise((resolve, reject) => {
10-
const retryDelay = 50;
11-
const maxRetries = 20; // Give up after 1 second
12-
13-
let retryCount = 0;
14-
15-
const checkServer = () => {
16-
setTimeout(() => {
17-
http.request({
18-
method: 'HEAD',
19-
hostname,
20-
port,
21-
path: pathname,
22-
}, response => {
23-
const statusCodeType = Number.parseInt(response.statusCode.toString()[0], 10);
10+
(async () => {
11+
const retryDelay = 50;
12+
const maxRetries = 20; // Give up after 1 second
13+
let retryCount = 0;
14+
15+
const fetchErrorDetails = async statusCode => {
16+
try {
17+
const response = await fetch(`http://${hostname}:${port}${pathname}`);
18+
const body = await response.text();
19+
// Extract the error message from the HTML response
20+
// PHP errors are wrapped in <b> tags: "<b>Fatal error</b>: ..."
21+
const errorMatch = body.match(/<b>(Fatal error|Parse error|Warning|Notice)<\/b>:\s*([^<\n]+)/);
22+
if (errorMatch) {
23+
return `Server returned ${statusCode} error: ${errorMatch[1]}: ${errorMatch[2]}`;
24+
}
25+
26+
return `Server returned ${statusCode} error. Please check your PHP application for possible errors.`;
27+
} catch {
28+
return `Server returned ${statusCode} error. Could not fetch error details.`;
29+
}
30+
};
31+
32+
const checkServer = async () => {
33+
try {
34+
const response = await fetch(`http://${hostname}:${port}${pathname}`, {
35+
method: 'HEAD',
36+
});
37+
38+
const statusCodeType = Number.parseInt(response.status.toString()[0], 10);
2439
if ([2, 3, 4].includes(statusCodeType)) {
2540
resolve();
2641
return;
2742
}
2843

2944
if (statusCodeType === 5) {
30-
reject(new Error('Server docroot returned 500-level response. Please check your configuration for possible errors.'));
45+
const errorMessage = await fetchErrorDetails(response.status);
46+
reject(new Error(errorMessage));
3147
return;
3248
}
3349

34-
checkServer();
35-
}).on('error', error => {
50+
await setTimeout(retryDelay);
51+
await checkServer();
52+
} catch (error) {
3653
if (++retryCount > maxRetries) {
3754
reject(new Error(`Could not start the PHP server: ${error.message}`));
3855
return;
3956
}
4057

41-
checkServer();
42-
}).end();
43-
}, retryDelay);
44-
};
58+
await setTimeout(retryDelay);
59+
await checkServer();
60+
}
61+
};
4562

46-
checkServer();
63+
await checkServer();
64+
})();
4765
});
4866

4967
export default async function phpServer(options) {
@@ -107,7 +125,12 @@ export default async function phpServer(options) {
107125

108126
// Check when the server is ready. Tried doing it by listening
109127
// to the child process `data` event, but it's not triggered...
110-
await isServerRunning(options.hostname, options.port, pathname);
128+
try {
129+
await isServerRunning(options.hostname, options.port, pathname);
130+
} catch (error) {
131+
subprocess.kill();
132+
throw error;
133+
}
111134

112135
if (options.open) {
113136
await open(`${url}${pathname}`);

package.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"node": ">=18"
2121
},
2222
"scripts": {
23-
"test": "xo && ava && tsd"
23+
"test": "xo && node --test test/test.js && tsd"
2424
},
2525
"files": [
2626
"index.js",
@@ -42,12 +42,8 @@
4242
},
4343
"devDependencies": {
4444
"@types/node": "^20.12.3",
45-
"ava": "^6.1.2",
4645
"got": "^14.2.1",
4746
"tsd": "^0.31.0",
4847
"xo": "^0.58.0"
49-
},
50-
"ava": {
51-
"workerThreads": false
5248
}
5349
}

test/fixtures/500/index.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
http_response_code(500);
3+
// This will trigger a fatal error (undefined function)
4+
undefined_function();
5+
echo "This should never be reached";
6+
?>

test/test.js

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,101 @@
11
import process from 'node:process';
22
import {fileURLToPath} from 'node:url';
33
import path from 'node:path';
4-
import test from 'ava';
4+
import {test, after} from 'node:test';
5+
import assert from 'node:assert/strict';
56
import got from 'got';
67
import phpServer from '../index.js';
78

89
const __dirname = path.dirname(fileURLToPath(import.meta.url));
910

1011
process.chdir(__dirname);
1112

12-
test('start a PHP server', async t => {
13+
// Track all servers created during tests
14+
const activeServers = new Set();
15+
16+
// Cleanup hook to ensure all servers are stopped
17+
after(() => {
18+
for (const server of activeServers) {
19+
server.stop();
20+
}
21+
22+
activeServers.clear();
23+
});
24+
25+
test('start a PHP server', async () => {
1326
const server = await phpServer({base: 'fixtures/200'});
27+
activeServers.add(server);
1428
const {statusCode, body} = await got(server.url);
15-
t.is(statusCode, 200);
16-
t.is(body, 'Hello World');
29+
assert.equal(statusCode, 200);
30+
assert.equal(body, 'Hello World');
1731
server.stop();
32+
activeServers.delete(server);
1833
});
1934

20-
test('start a PHP server when the status code is 301', async t => {
35+
test('start a PHP server when the status code is 301', async () => {
2136
const server = await phpServer({base: 'fixtures/301'});
37+
activeServers.add(server);
2238
const {statusCode, body} = await got(server.url);
23-
t.is(statusCode, 200);
24-
t.is(body, '301 Redirected!');
39+
assert.equal(statusCode, 200);
40+
assert.equal(body, '301 Redirected!');
2541
server.stop();
42+
activeServers.delete(server);
2643
});
2744

28-
test('start a PHP server when the status code is 400', async t => {
45+
test('start a PHP server when the status code is 400', async () => {
2946
const server = await phpServer({base: 'fixtures/400'});
47+
activeServers.add(server);
3048
const {statusCode} = await got(server.url, {throwHttpErrors: false});
31-
t.is(statusCode, 400);
49+
assert.equal(statusCode, 400);
3250
server.stop();
51+
activeServers.delete(server);
3352
});
3453

35-
test('start a PHP server when the status code is 404', async t => {
54+
test('start a PHP server when the status code is 404', async () => {
3655
const server = await phpServer({base: 'fixtures/404'});
56+
activeServers.add(server);
3757
const {statusCode} = await got(server.url, {throwHttpErrors: false});
38-
t.is(statusCode, 404);
58+
assert.equal(statusCode, 404);
3959
server.stop();
60+
activeServers.delete(server);
4061
});
4162

42-
test('expose environment variables', async t => {
63+
test('expose environment variables', async () => {
4364
const server = await phpServer({
4465
base: 'fixtures/env',
4566
env: {
4667
FOOBAR: 'foobar',
4768
},
4869
});
70+
activeServers.add(server);
4971
const {body} = await got(server.url);
50-
t.is(body, 'foobar');
72+
assert.equal(body, 'foobar');
5173
server.stop();
74+
activeServers.delete(server);
5275
});
5376

54-
test('expose custom INI directive', async t => {
77+
test('expose custom INI directive', async () => {
5578
const server = await phpServer({
5679
base: 'fixtures/directives',
5780
directives: {
5881
error_log: 'foobar', // eslint-disable-line camelcase
5982
},
6083
});
84+
activeServers.add(server);
6185
const {body} = await got(server.url);
62-
t.is(body, 'foobar');
86+
assert.equal(body, 'foobar');
6387
server.stop();
88+
activeServers.delete(server);
89+
});
90+
91+
test('show detailed error message when the status code is 500', async () => {
92+
await assert.rejects(
93+
phpServer({base: 'fixtures/500'}),
94+
error => {
95+
// Should contain "Fatal error" and "undefined_function"
96+
assert.match(error.message, /500 error: Fatal error/);
97+
assert.match(error.message, /undefined_function/);
98+
return true;
99+
},
100+
);
64101
});

0 commit comments

Comments
 (0)