Skip to content

Commit dd4b1ae

Browse files
committed
Improve the error messages for command I/O errors
Similar to #355 but for I/O errors from spawning a `Command` instead of from file operations. Such Command I/O errors are only likely to happen in the case of a bug, so the error message now says as much. The error also now generates the program name from the command, so avoid it getting out of sync. The command's args are intentionally omitted, since they aren't relevant for errors that occur when spawning a command. (ie: For command not found, it's the program that's not found, not the args.) GUS-W-12650236.
1 parent 7d957ec commit dd4b1ae

File tree

3 files changed

+72
-83
lines changed

3 files changed

+72
-83
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Changed
1515

1616
- Improved the error messages shown when `.python-version` contains an invalid Python version or stray invisible characters (such as ASCII control codes). ([#353](https://github.com/heroku/buildpacks-python/pull/353) and [#354](https://github.com/heroku/buildpacks-python/pull/354))
17-
- Improved the error messages shown if I/O errors occur. ([#355](https://github.com/heroku/buildpacks-python/pull/355))
17+
- Improved the error messages shown if I/O errors occur. ([#355](https://github.com/heroku/buildpacks-python/pull/355) and [#356](https://github.com/heroku/buildpacks-python/pull/356))
1818

1919
## [0.26.1] - 2025-04-08
2020

src/errors.rs

Lines changed: 49 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,27 @@ use crate::python_version::{
1414
};
1515
use crate::python_version_file::ParsePythonVersionFileError;
1616
use crate::utils::{
17-
CapturedCommandError, DownloadUnpackArchiveError, FileExistsError, FindBundledPipError,
18-
ReadOptionalFileError, StreamedCommandError,
17+
CapturedCommandError, CommandIoError, DownloadUnpackArchiveError, FileExistsError,
18+
FindBundledPipError, ReadOptionalFileError, StreamedCommandError,
1919
};
2020
use indoc::{formatdoc, indoc};
2121
use libherokubuildpack::log::log_error;
22-
use std::io;
2322

2423
/// Handle any non-recoverable buildpack or libcnb errors that occur.
2524
///
2625
/// The buildpack will exit non-zero after this handler has run, so all that needs to be
2726
/// performed here is the logging of an error message - and in the future, emitting metrics.
28-
///
29-
/// We're intentionally not using `libherokubuildpack::error::on_error` since:
30-
/// - It doesn't currently do anything other than logging an internal error for the libcnb
31-
/// error case, and by inlining that here it's easier to keep the output consistent with
32-
/// the messages emitted for buildpack-specific errors.
33-
/// - Using it causes trait mismatch errors when Dependabot PRs incrementally update crates.
34-
/// - When we want to add metrics to our buildpacks, it's going to need a rewrite of
35-
/// `Buildpack::on_error` anyway (we'll need to write out metrics not log them, so will need
36-
/// access to the `BuildContext`), at which point we can re-evaluate.
3727
pub(crate) fn on_error(error: libcnb::Error<BuildpackError>) {
3828
match error {
3929
libcnb::Error::BuildpackError(buildpack_error) => on_buildpack_error(buildpack_error),
4030
libcnb_error => log_error(
4131
"Internal buildpack error",
4232
formatdoc! {"
43-
An unexpected internal error was reported by the framework used by this buildpack.
44-
45-
Please open a support ticket and include the full log output of this build.
46-
33+
An error was reported by the framework used by this buildpack.
34+
4735
Details: {libcnb_error}
36+
37+
{INTERNAL_ERROR_MESSAGE}
4838
"},
4939
),
5040
}
@@ -303,10 +293,16 @@ fn on_python_layer_error(error: PythonLayerError) {
303293
Details: {ureq_error}
304294
"},
305295
),
306-
DownloadUnpackArchiveError::Unpack(io_error) => log_io_error(
296+
DownloadUnpackArchiveError::Unpack(io_error) => log_error(
307297
"Unable to unpack the Python archive",
308-
"unpacking the downloaded Python runtime archive and writing it to disk",
309-
&io_error,
298+
// TODO: Investigate under what circumstances this error can occur, and so whether
299+
// we should label this as an internal error or else list suggested actions.
300+
formatdoc! {"
301+
An I/O error occurred while unpacking the downloaded Python
302+
runtime archive and writing it to disk.
303+
304+
Details: I/O Error: {io_error}
305+
"},
310306
),
311307
},
312308
// TODO: Remove this once versions are validated against a manifest (at which point all
@@ -347,11 +343,7 @@ fn on_python_layer_error(error: PythonLayerError) {
347343
fn on_pip_layer_error(error: PipLayerError) {
348344
match error {
349345
PipLayerError::InstallPipCommand(error) => match error {
350-
StreamedCommandError::Io(io_error) => log_io_error(
351-
"Unable to install pip",
352-
"running 'python' to install pip",
353-
&io_error,
354-
),
346+
StreamedCommandError::Io(error) => log_command_io_error(error),
355347
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
356348
"Unable to install pip",
357349
formatdoc! {"
@@ -375,11 +367,7 @@ fn on_pip_layer_error(error: PipLayerError) {
375367
fn on_pip_dependencies_layer_error(error: PipDependenciesLayerError) {
376368
match error {
377369
PipDependenciesLayerError::CreateVenvCommand(error) => match error {
378-
StreamedCommandError::Io(io_error) => log_io_error(
379-
"Unable to create virtual environment",
380-
"running 'python -m venv' to create a virtual environment",
381-
&io_error,
382-
),
370+
StreamedCommandError::Io(error) => log_command_io_error(error),
383371
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
384372
"Unable to create virtual environment",
385373
formatdoc! {"
@@ -391,11 +379,7 @@ fn on_pip_dependencies_layer_error(error: PipDependenciesLayerError) {
391379
),
392380
},
393381
PipDependenciesLayerError::PipInstallCommand(error) => match error {
394-
StreamedCommandError::Io(io_error) => log_io_error(
395-
"Unable to install dependencies using pip",
396-
"running 'pip install' to install the app's dependencies",
397-
&io_error,
398-
),
382+
StreamedCommandError::Io(error) => log_command_io_error(error),
399383
// TODO: Add more suggestions here as to causes (eg network, invalid requirements.txt,
400384
// package broken or not compatible with version of Python, missing system dependencies etc)
401385
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
@@ -414,11 +398,7 @@ fn on_pip_dependencies_layer_error(error: PipDependenciesLayerError) {
414398
fn on_poetry_layer_error(error: PoetryLayerError) {
415399
match error {
416400
PoetryLayerError::InstallPoetryCommand(error) => match error {
417-
StreamedCommandError::Io(io_error) => log_io_error(
418-
"Unable to install Poetry",
419-
"running 'python' to install Poetry",
420-
&io_error,
421-
),
401+
StreamedCommandError::Io(error) => log_command_io_error(error),
422402
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
423403
"Unable to install Poetry",
424404
formatdoc! {"
@@ -442,11 +422,7 @@ fn on_poetry_layer_error(error: PoetryLayerError) {
442422
fn on_poetry_dependencies_layer_error(error: PoetryDependenciesLayerError) {
443423
match error {
444424
PoetryDependenciesLayerError::CreateVenvCommand(error) => match error {
445-
StreamedCommandError::Io(io_error) => log_io_error(
446-
"Unable to create virtual environment",
447-
"running 'python -m venv' to create a virtual environment",
448-
&io_error,
449-
),
425+
StreamedCommandError::Io(error) => log_command_io_error(error),
450426
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
451427
"Unable to create virtual environment",
452428
formatdoc! {"
@@ -458,11 +434,7 @@ fn on_poetry_dependencies_layer_error(error: PoetryDependenciesLayerError) {
458434
),
459435
},
460436
PoetryDependenciesLayerError::PoetryInstallCommand(error) => match error {
461-
StreamedCommandError::Io(io_error) => log_io_error(
462-
"Unable to install dependencies using Poetry",
463-
"running 'poetry install' to install the app's dependencies",
464-
&io_error,
465-
),
437+
StreamedCommandError::Io(error) => log_command_io_error(error),
466438
// TODO: Add more suggestions here as to possible causes (similar to pip)
467439
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
468440
"Unable to install dependencies using Poetry",
@@ -480,11 +452,7 @@ fn on_poetry_dependencies_layer_error(error: PoetryDependenciesLayerError) {
480452
fn on_django_collectstatic_error(error: DjangoCollectstaticError) {
481453
match error {
482454
DjangoCollectstaticError::CheckCollectstaticCommandExists(error) => match error {
483-
CapturedCommandError::Io(io_error) => log_io_error(
484-
"Unable to inspect Django configuration",
485-
"running 'python manage.py help collectstatic' to inspect the Django configuration",
486-
&io_error,
487-
),
455+
CapturedCommandError::Io(error) => log_command_io_error(error),
488456
CapturedCommandError::NonZeroExitStatus(output) => log_error(
489457
"Unable to inspect Django configuration",
490458
formatdoc! {"
@@ -509,11 +477,7 @@ fn on_django_collectstatic_error(error: DjangoCollectstaticError) {
509477
log_file_exists_error(error);
510478
}
511479
DjangoCollectstaticError::CollectstaticCommand(error) => match error {
512-
StreamedCommandError::Io(io_error) => log_io_error(
513-
"Unable to generate Django static files",
514-
"running 'python manage.py collectstatic' to generate Django static files",
515-
&io_error,
516-
),
480+
StreamedCommandError::Io(error) => log_command_io_error(error),
517481
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
518482
"Unable to generate Django static files",
519483
formatdoc! {"
@@ -537,21 +501,6 @@ fn on_django_collectstatic_error(error: DjangoCollectstaticError) {
537501
}
538502
}
539503

540-
// This is now only used for Command I/O errors.
541-
// TODO: Replace this with specialised handling for Command I/O errors.
542-
fn log_io_error(header: &str, occurred_whilst: &str, io_error: &io::Error) {
543-
// We don't suggest opening a support ticket, since a subset of I/O errors can be caused
544-
// by issues in the application. In the future, perhaps we should try and split these out?
545-
log_error(
546-
header,
547-
formatdoc! {"
548-
An unexpected error occurred whilst {occurred_whilst}.
549-
550-
Details: I/O Error: {io_error}
551-
"},
552-
);
553-
}
554-
555504
fn log_file_exists_error(FileExistsError { path, io_error }: FileExistsError) {
556505
let filepath = path.to_string_lossy();
557506
let filename = path
@@ -567,7 +516,7 @@ fn log_file_exists_error(FileExistsError { path, io_error }: FileExistsError) {
567516
568517
Details: {io_error}
569518
570-
Try building again to see if the error resolves itself.
519+
{INTERNAL_ERROR_MESSAGE}
571520
"},
572521
);
573522
}
@@ -602,7 +551,6 @@ fn log_find_bundled_pip_error(
602551
) {
603552
let bundled_wheels_dir = bundled_wheels_dir.to_string_lossy();
604553

605-
// TODO: Decide whether we want users to file a bug or open a support ticket.
606554
log_error(
607555
"Unable to locate the Python stdlib's bundled pip",
608556
formatdoc! {"
@@ -612,9 +560,32 @@ fn log_find_bundled_pip_error(
612560
613561
Details: {io_error}
614562
615-
This is an internal error. Please report it as a bug, here:
616-
https://github.com/heroku/buildpacks-python/issues
563+
{INTERNAL_ERROR_MESSAGE}
617564
"
618565
},
619566
);
620567
}
568+
569+
fn log_command_io_error(CommandIoError { program, io_error }: CommandIoError) {
570+
log_error(
571+
format!("Unable to run {program}"),
572+
formatdoc! {"
573+
An I/O error occurred while trying to run:
574+
`{program}`
575+
576+
Details: {io_error}
577+
578+
{INTERNAL_ERROR_MESSAGE}
579+
"},
580+
);
581+
}
582+
583+
const INTERNAL_ERROR_MESSAGE: &str = indoc! {"
584+
This is an unexpected error that could be caused by a bug
585+
in this buildpack, or an issue with the build environment.
586+
587+
Try building again to see if the error resolves itself.
588+
589+
If it doesn't, please file a bug report here:
590+
https://github.com/heroku/buildpacks-python/issues
591+
"};

src/utils.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ pub(crate) fn run_command_and_stream_output(
156156
) -> Result<(), StreamedCommandError> {
157157
command
158158
.status()
159-
.map_err(StreamedCommandError::Io)
159+
.map_err(|io_error| {
160+
StreamedCommandError::Io(CommandIoError {
161+
program: command.get_program().to_string_lossy().to_string(),
162+
io_error,
163+
})
164+
})
160165
.and_then(|exit_status| {
161166
if exit_status.success() {
162167
Ok(())
@@ -173,7 +178,12 @@ pub(crate) fn run_command_and_capture_output(
173178
) -> Result<Output, CapturedCommandError> {
174179
command
175180
.output()
176-
.map_err(CapturedCommandError::Io)
181+
.map_err(|io_error| {
182+
CapturedCommandError::Io(CommandIoError {
183+
program: command.get_program().to_string_lossy().to_string(),
184+
io_error,
185+
})
186+
})
177187
.and_then(|output| {
178188
if output.status.success() {
179189
Ok(output)
@@ -186,17 +196,25 @@ pub(crate) fn run_command_and_capture_output(
186196
/// Errors that can occur when running an external process using `run_command_and_stream_output`.
187197
#[derive(Debug)]
188198
pub(crate) enum StreamedCommandError {
189-
Io(io::Error),
199+
Io(CommandIoError),
190200
NonZeroExitStatus(ExitStatus),
191201
}
192202

193203
/// Errors that can occur when running an external process using `run_command_and_capture_output`.
194204
#[derive(Debug)]
195205
pub(crate) enum CapturedCommandError {
196-
Io(io::Error),
206+
Io(CommandIoError),
197207
NonZeroExitStatus(Output),
198208
}
199209

210+
/// I/O error that occurred while spawning/waiting on a command,
211+
/// such as when the program wasn't found.
212+
#[derive(Debug)]
213+
pub(crate) struct CommandIoError {
214+
pub(crate) program: String,
215+
pub(crate) io_error: io::Error,
216+
}
217+
200218
/// Convert a [`libcnb::Env`] to a sorted vector of key-value string slice tuples, for easier
201219
/// testing of the environment variables set in the buildpack layers.
202220
#[cfg(test)]

0 commit comments

Comments
 (0)