Skip to content

Commit 258dfa0

Browse files
authored
Improve error messages and metrics for unhandled errors (#1933)
Adds a Bash ERR trap to catch any unhandled buildpack errors, which displays a more helpful error message (that includes the command being run and the stack trace), as well as setting an appropriate failure reason and detail in the build event payload. For example: ``` -----> Using Python 3.13 specified in .python-version -----> Installing Python 3.13.8 mkdir: cannot create directory ‘/tmp/build_1/.heroku’: Not a directory ! Internal Error: An unhandled buildpack error occurred. ! ! An unhandled error occurred while executing the command: ! mkdir -p "${install_dir}" ! ! If this issue persists, please open a support ticket or file ! an issue on the buildpack's GitHub repository: ! https://help.heroku.com/ ! https://github.com/heroku/heroku-buildpack-python/issues ! ! Stack trace: ! python::install @ /tmp/buildpack/lib/python.sh:25 ! main @ ./bin/compile:170 ~ Report: { ... "failure_detail": "mkdir -p \"${install_dir}\"", "failure_reason": "internal-error::unhandled", ... } ``` See: - https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap - https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-caller - https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html - https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html GUS-W-19792272. GUS-W-19792285.
1 parent fc63f31 commit 258dfa0

4 files changed

Lines changed: 55 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Improved error messages and metrics for unhandled buildpack errors. ([#1933](https://github.com/heroku/heroku-buildpack-python/pull/1933))
56

67
## [v313] - 2025-10-09
78

bin/compile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
# Usage: bin/compile <build-dir> <cache-dir> <env-dir>
33
# See: https://devcenter.heroku.com/articles/buildpack-api
44

5-
set -euo pipefail
5+
# We use `errtrace` and `inherit_errexit` to ensure the ERR trap and exit on error behaviour
6+
# propagates to buildpack functions run in subshells, such when using command substitutions.
7+
set -eu -o pipefail -o errtrace
68
shopt -s inherit_errexit
79

810
# Note: This can't be enabled via app config vars, since at this point they haven't been loaded from ENV_DIR.
@@ -39,6 +41,8 @@ compile_start_time=$(build_data::current_unix_realtime)
3941
# a build report that will be consumed by the build system for observability.
4042
build_data::setup
4143

44+
trap 'utils::err_trap' ERR
45+
4246
checks::ensure_supported_stack "${STACK:?"Required env var STACK isn't set"}"
4347
checks::duplicate_python_buildpack "${BUILD_DIR}"
4448

lib/build_data.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ function build_data::_set() {
109109
echo "${new_data_file_contents}" >"${BUILD_DATA_FILE}"
110110
}
111111

112+
# Check whether an entry exists in the build data store for the current build.
113+
# Returns zero if the key was found and non-zero otherwise.
114+
#
115+
# Usage:
116+
# ```
117+
# build_data::has "failure_reason"
118+
# ```
119+
function build_data::has() {
120+
local key="${1}"
121+
122+
jq --exit-status "has(\"${key}\")" "${BUILD_DATA_FILE}" >/dev/null
123+
}
124+
112125
# Retrieve the value of an entry in the build data store from the previous successful build.
113126
# Returns the empty string if the key wasn't found in the store.
114127
#

lib/utils.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,39 @@ function utils::abort_internal_error() {
6666
build_data::set_string "failure_detail" "${message}"
6767
exit 1
6868
}
69+
70+
function utils::err_trap() {
71+
# We use `errtrace` which means the ERR trap can fire multiple times, such as when an error
72+
# occurs in a buildpack function called from within a command substitution.
73+
# shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled.
74+
if build_data::has "failure_reason"; then
75+
return
76+
fi
77+
78+
local failing_command="${BASH_COMMAND}"
79+
local stack_trace
80+
stack_trace=$(
81+
local frame=0
82+
while read -r line_number function_name source_file < <(caller "${frame}" || true); do
83+
echo "${function_name} @ ${source_file}:${line_number}"
84+
((++frame))
85+
done
86+
)
87+
88+
output::error <<-EOF
89+
Internal Error: An unhandled buildpack error occurred.
90+
91+
An unhandled error occurred while executing the command:
92+
${failing_command}
93+
94+
If this issue persists, please open a support ticket or file
95+
an issue on the buildpack's GitHub repository:
96+
https://help.heroku.com/
97+
https://github.com/heroku/heroku-buildpack-python/issues
98+
99+
Stack trace:
100+
${stack_trace}
101+
EOF
102+
build_data::set_string "failure_reason" "internal-error::unhandled"
103+
build_data::set_string "failure_detail" "${failing_command}"
104+
}

0 commit comments

Comments
 (0)