Skip to content

Commit 4030374

Browse files
authored
Support returning responses with HTTP codes rather than errors (#30)
1 parent b1437ad commit 4030374

File tree

11 files changed

+350
-211
lines changed

11 files changed

+350
-211
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ console.log(response.body.toString())
6060
* `config` {Object} Configuration object
6161
* `argv` {String[]} Process arguments. **Default:** []
6262
* `docroot` {String} Document root for PHP. **Default:** process.cwd()
63+
* `throwRequestErrors` {Boolean} Throw request errors rather than returning
64+
responses with error codes. **Default:** false
6365
* Returns: {Php}
6466

6567
Construct a new PHP instance to which to dispatch requests.

__test__/handler.spec.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,41 @@ test('Has expected args', async (t) => {
129129

130130
t.is(res.body.toString('utf8'), JSON.stringify(process.argv))
131131
})
132+
133+
test('Not found script returns response with status', async (t) => {
134+
const mockroot = await MockRoot.from({})
135+
t.teardown(() => mockroot.clean())
136+
137+
const php = new Php({
138+
argv: process.argv,
139+
docroot: mockroot.path
140+
})
141+
142+
const req = new Request({
143+
url: 'http://example.com/index.php'
144+
})
145+
146+
const res = await php.handleRequest(req)
147+
t.is(res.status, 404)
148+
149+
t.is(res.body.toString('utf8'), 'Not Found')
150+
})
151+
152+
test('Allow receiving true errors', async (t) => {
153+
const mockroot = await MockRoot.from({})
154+
t.teardown(() => mockroot.clean())
155+
156+
const php = new Php({
157+
argv: process.argv,
158+
docroot: mockroot.path,
159+
throwRequestErrors: true
160+
})
161+
162+
const req = new Request({
163+
url: 'http://example.com/index.php'
164+
})
165+
166+
await t.throwsAsync(() => php.handleRequest(req), {
167+
message: /^Script not found: .*\/index\.php$/
168+
}, 'should throw error')
169+
})

crates/php/src/embed.rs

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88

99
use ext_php_rs::{
1010
error::Error,
11-
ffi::{php_execute_script, php_module_shutdown, sapi_get_default_content_type},
11+
ffi::{php_execute_script, sapi_get_default_content_type},
1212
zend::{try_catch, try_catch_first, ExecutorGlobals, SapiGlobals},
1313
};
1414

@@ -17,8 +17,8 @@ use lang_handler::{Handler, Request, Response};
1717
use super::{
1818
sapi::{ensure_sapi, Sapi},
1919
scopes::{FileHandleScope, RequestScope},
20-
strings::{cstr, nullable_cstr, str_from_cstr, translate_path},
21-
EmbedException, RequestContext,
20+
strings::{cstr, nullable_cstr, translate_path},
21+
EmbedRequestError, EmbedStartError, RequestContext,
2222
};
2323

2424
/// Embed a PHP script into a Rust application to handle HTTP requests.
@@ -51,7 +51,7 @@ impl Embed {
5151
///
5252
/// let embed = Embed::new(docroot);
5353
/// ```
54-
pub fn new<C: AsRef<Path>>(docroot: C) -> Result<Self, EmbedException> {
54+
pub fn new<C: AsRef<Path>>(docroot: C) -> Result<Self, EmbedStartError> {
5555
Embed::new_with_argv::<C, String>(docroot, vec![])
5656
}
5757

@@ -68,7 +68,7 @@ impl Embed {
6868
///
6969
/// let embed = Embed::new_with_args(docroot, args());
7070
/// ```
71-
pub fn new_with_args<C>(docroot: C, args: Args) -> Result<Self, EmbedException>
71+
pub fn new_with_args<C>(docroot: C, args: Args) -> Result<Self, EmbedStartError>
7272
where
7373
C: AsRef<Path>,
7474
{
@@ -90,15 +90,15 @@ impl Embed {
9090
/// "foo"
9191
/// ]);
9292
/// ```
93-
pub fn new_with_argv<C, S>(docroot: C, argv: Vec<S>) -> Result<Self, EmbedException>
93+
pub fn new_with_argv<C, S>(docroot: C, argv: Vec<S>) -> Result<Self, EmbedStartError>
9494
where
9595
C: AsRef<Path>,
9696
S: AsRef<str> + std::fmt::Debug,
9797
{
9898
let docroot_path = docroot.as_ref();
9999
let docroot = docroot_path
100100
.canonicalize()
101-
.map_err(|_| EmbedException::DocRootNotFound(docroot_path.display().to_string()))?;
101+
.map_err(|_| EmbedStartError::DocRootNotFound(docroot_path.display().to_string()))?;
102102

103103
Ok(Embed {
104104
docroot,
@@ -129,7 +129,7 @@ impl Embed {
129129
}
130130

131131
impl Handler for Embed {
132-
type Error = EmbedException;
132+
type Error = EmbedRequestError;
133133

134134
/// Handles an HTTP request.
135135
///
@@ -169,34 +169,40 @@ impl Handler for Embed {
169169
let translated_path = translate_path(&self.docroot, request_uri)?
170170
.display()
171171
.to_string();
172-
let path_translated = cstr(translated_path.clone())?;
173-
let request_uri = cstr(request_uri)?;
172+
let path_translated = cstr(translated_path.clone())
173+
.map_err(|_| EmbedRequestError::FailedToSetRequestInfo("path_translated".into()))?;
174+
let request_uri = cstr(request_uri)
175+
.map_err(|_| EmbedRequestError::FailedToSetRequestInfo("request_uri".into()))?;
174176

175177
// Extract request method, query string, and headers
176-
let request_method = cstr(request.method())?;
177-
let query_string = cstr(url.query().unwrap_or(""))?;
178+
let request_method = cstr(request.method())
179+
.map_err(|_| EmbedRequestError::FailedToSetRequestInfo("request_method".into()))?;
180+
let query_string = cstr(url.query().unwrap_or(""))
181+
.map_err(|_| EmbedRequestError::FailedToSetRequestInfo("query_string".into()))?;
178182

179183
let headers = request.headers();
180-
let content_type = nullable_cstr(headers.get("Content-Type"))?;
184+
let content_type = nullable_cstr(headers.get("Content-Type"))
185+
.map_err(|_| EmbedRequestError::FailedToSetRequestInfo("content_type".into()))?;
181186
let content_length = headers
182187
.get("Content-Length")
183188
.map(|v| v.parse::<i64>().unwrap_or(0))
184189
.unwrap_or(0);
185-
let cookie_data = nullable_cstr(headers.get("Cookie"))?;
190+
let cookie_data = nullable_cstr(headers.get("Cookie"))
191+
.map_err(|_| EmbedRequestError::FailedToSetRequestInfo("cookie_data".into()))?;
186192

187193
// Prepare argv and argc
188194
let argc = self.args.len() as i32;
189195
let mut argv_ptrs = vec![];
190196
for arg in self.args.iter() {
191197
let string = CString::new(arg.as_bytes())
192-
.map_err(|_| EmbedException::CStringEncodeFailed(arg.to_owned()))?;
198+
.map_err(|_| EmbedRequestError::FailedToSetRequestInfo("argv".into()))?;
193199
argv_ptrs.push(string.into_raw());
194200
}
195201

196202
let script_name = translated_path.clone();
197203

198204
let response = try_catch_first(|| {
199-
RequestContext::for_request(request.clone());
205+
RequestContext::for_request(request.clone(), self.docroot.clone());
200206

201207
// Set server context
202208
{
@@ -247,12 +253,12 @@ impl Handler for Embed {
247253

248254
// TODO: Should exceptions be raised or only captured on
249255
// the response builder?
250-
return Err(EmbedException::Exception(ex.to_string()));
256+
return Err(EmbedRequestError::Exception(ex.to_string()));
251257
}
252258

253259
Ok(())
254260
})
255-
.unwrap_or(Err(EmbedException::Bailout))?;
261+
.unwrap_or(Err(EmbedRequestError::Bailout))?;
256262

257263
let (mimetype, http_response_code) = {
258264
let globals = SapiGlobals::get();
@@ -262,13 +268,13 @@ impl Handler for Embed {
262268
)
263269
};
264270

265-
let default_mime = str_from_cstr(unsafe { sapi_get_default_content_type() })?;
271+
let mime_ptr = unsafe { (mimetype as *mut std::ffi::c_char).as_ref() }
272+
.or(unsafe { sapi_get_default_content_type().as_ref() })
273+
.ok_or(EmbedRequestError::FailedToDetermineContentType)?;
266274

267-
let mime = if mimetype.is_null() {
268-
default_mime
269-
} else {
270-
str_from_cstr(mimetype).unwrap_or(default_mime)
271-
};
275+
let mime = unsafe { std::ffi::CStr::from_ptr(mime_ptr) }
276+
.to_str()
277+
.map_err(|_| EmbedRequestError::FailedToDetermineContentType)?;
272278

273279
RequestContext::current()
274280
.map(|ctx| {
@@ -277,12 +283,12 @@ impl Handler for Embed {
277283
.status(http_response_code)
278284
.header("Content-Type", mime)
279285
})
280-
.ok_or(EmbedException::ResponseBuildError)?
286+
.ok_or(EmbedRequestError::ResponseBuildError)?
281287
};
282288

283289
Ok(response_builder.build())
284290
})
285-
.unwrap_or(Err(EmbedException::Bailout))?;
291+
.unwrap_or(Err(EmbedRequestError::Bailout))?;
286292

287293
RequestContext::reclaim();
288294

crates/php/src/exception.rs

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,74 @@
11
/// Set of exceptions which may be produced by php::Embed
22
#[derive(Debug)]
3-
pub enum EmbedException {
3+
pub enum EmbedStartError {
44
DocRootNotFound(String),
5+
ExeLocationNotFound,
56
SapiNotInitialized,
7+
}
8+
9+
impl std::fmt::Display for EmbedStartError {
10+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11+
match self {
12+
EmbedStartError::DocRootNotFound(docroot) => {
13+
write!(f, "Document root not found: {}", docroot)
14+
}
15+
EmbedStartError::ExeLocationNotFound => {
16+
write!(f, "Failed to identify executable location")
17+
}
18+
EmbedStartError::SapiNotInitialized => write!(f, "Failed to initialize SAPI"),
19+
}
20+
}
21+
}
22+
23+
#[derive(Debug)]
24+
pub enum EmbedRequestError {
625
SapiNotStarted,
7-
SapiLockFailed,
8-
SapiMissingStartupFunction,
9-
FailedToFindExeLocation,
26+
SapiNotShutdown,
1027
SapiRequestNotStarted,
1128
RequestContextUnavailable,
1229
CStringEncodeFailed(String),
13-
CStringDecodeFailed(usize),
14-
HeaderNotFound(String),
1530
// ExecuteError,
1631
Exception(String),
1732
Bailout,
1833
ResponseBuildError,
1934
FailedToFindCurrentDirectory,
2035
ExpectedAbsoluteRequestUri(String),
2136
ScriptNotFound(String),
37+
FailedToDetermineContentType,
38+
FailedToSetServerVar(String),
39+
FailedToSetRequestInfo(String),
2240
}
2341

24-
impl std::fmt::Display for EmbedException {
42+
impl std::fmt::Display for EmbedRequestError {
2543
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2644
match self {
27-
EmbedException::RequestContextUnavailable => write!(f, "Request context unavailable"),
28-
EmbedException::SapiNotInitialized => write!(f, "Failed to initialize SAPI"),
29-
EmbedException::SapiLockFailed => write!(f, "Failed to acquire SAPI lock"),
30-
EmbedException::SapiMissingStartupFunction => write!(f, "Missing SAPI startup function"),
31-
EmbedException::FailedToFindExeLocation => {
32-
write!(f, "Failed to identify executable location")
45+
EmbedRequestError::SapiNotStarted => write!(f, "Failed to start SAPI"),
46+
EmbedRequestError::SapiNotShutdown => write!(f, "Failed to shutdown SAPI"),
47+
EmbedRequestError::RequestContextUnavailable => write!(f, "Request context unavailable"),
48+
EmbedRequestError::SapiRequestNotStarted => write!(f, "Failed to start SAPI request"),
49+
EmbedRequestError::CStringEncodeFailed(e) => {
50+
write!(f, "Failed to encode to cstring: \"{}\"", e)
3351
}
34-
EmbedException::DocRootNotFound(docroot) => write!(f, "Document root not found: {}", docroot),
35-
EmbedException::SapiNotStarted => write!(f, "Failed to start SAPI"),
36-
EmbedException::SapiRequestNotStarted => write!(f, "Failed to start SAPI request"),
37-
EmbedException::CStringEncodeFailed(e) => write!(f, "Failed to encode to cstring: \"{}\"", e),
38-
EmbedException::CStringDecodeFailed(e) => write!(f, "Failed to decode from cstring: {}", e),
39-
EmbedException::HeaderNotFound(header) => write!(f, "Header not found: {}", header),
40-
// EmbedException::ExecuteError => write!(f, "Script execution error"),
41-
EmbedException::Exception(e) => write!(f, "Exception thrown: {}", e),
42-
EmbedException::Bailout => write!(f, "PHP bailout"),
43-
EmbedException::ResponseBuildError => write!(f, "Failed to build response"),
44-
EmbedException::FailedToFindCurrentDirectory => {
52+
// EmbedRequestError::ExecuteError => write!(f, "Script execution error"),
53+
EmbedRequestError::Exception(e) => write!(f, "Exception thrown: {}", e),
54+
EmbedRequestError::Bailout => write!(f, "PHP bailout"),
55+
EmbedRequestError::ResponseBuildError => write!(f, "Failed to build response"),
56+
EmbedRequestError::FailedToFindCurrentDirectory => {
4557
write!(f, "Failed to identify current directory")
4658
}
47-
EmbedException::ExpectedAbsoluteRequestUri(e) => {
59+
EmbedRequestError::ExpectedAbsoluteRequestUri(e) => {
4860
write!(f, "Expected absolute REQUEST_URI: {}", e)
4961
}
50-
EmbedException::ScriptNotFound(e) => write!(f, "Script not found: {}", e),
62+
EmbedRequestError::ScriptNotFound(e) => write!(f, "Script not found: {}", e),
63+
EmbedRequestError::FailedToDetermineContentType => {
64+
write!(f, "Failed to determine content type")
65+
}
66+
EmbedRequestError::FailedToSetServerVar(name) => {
67+
write!(f, "Failed to set server var: \"{}\"", name)
68+
}
69+
EmbedRequestError::FailedToSetRequestInfo(name) => {
70+
write!(f, "Failed to set request info: \"{}\"", name)
71+
}
5172
}
5273
}
5374
}

crates/php/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ mod strings;
1111
pub use lang_handler::{Handler, Header, Headers, Request, RequestBuilder, Response, Url};
1212

1313
pub use embed::Embed;
14-
pub use exception::EmbedException;
14+
pub use exception::{EmbedRequestError, EmbedStartError};
1515
pub use request_context::RequestContext;

0 commit comments

Comments
 (0)