diff --git a/src/workerd/api/http.c++ b/src/workerd/api/http.c++ index ed24374e413..556fc74249a 100644 --- a/src/workerd/api/http.c++ +++ b/src/workerd/api/http.c++ @@ -1687,11 +1687,35 @@ jsg::Promise> fetchImplNoOutputLock(jsg::Lock& js, return ioContext.awaitIo(js, AbortSignal::maybeCancelWrap(js, signal, kj::mv(KJ_ASSERT_NONNULL(nativeRequest).response)) .catch_([](kj::Exception&& exception) -> kj::Promise { - if (exception.getDescription().startsWith("invalid Content-Length header value")) { + auto desc = exception.getDescription(); + + if (desc.startsWith("invalid Content-Length header value")) { return JSG_KJ_EXCEPTION(FAILED, Error, exception.getDescription()); - } else if (exception.getDescription().contains("NOSENTRY script not found")) { + } else if (desc.contains("NOSENTRY script not found")) { return JSG_KJ_EXCEPTION(FAILED, Error, "Worker not found."); } + // Handle TLS certificate validation errors with helpful messages + else if (desc.contains("TLS peer's certificate is not trusted")) { + return JSG_KJ_EXCEPTION(FAILED, Error, + "Network connection failed: The destination server's TLS certificate could not be " + "verified. If connecting to a server with a self-signed certificate in local " + "development, ensure NODE_EXTRA_CA_CERTS is set to a file containing the CA " + "certificate."); + } else if (desc.contains("TLS peer provided no certificate")) { + return JSG_KJ_EXCEPTION(FAILED, Error, + "Network connection failed: The destination server did not provide a TLS " + "certificate."); + } else if (desc.contains("peer disconnected without gracefully ending TLS session")) { + return JSG_KJ_EXCEPTION(FAILED, Error, + "Network connection failed: The connection was closed unexpectedly during TLS " + "handshake."); + } else if (desc.contains("OpenSSL error") || desc.contains("SSL_")) { + // Generic TLS/SSL error + return JSG_KJ_EXCEPTION(FAILED, Error, + "Network connection failed due to a TLS/SSL error. This may indicate a certificate " + "configuration issue. In local development, ensure the destination server's CA " + "certificate is trusted via NODE_EXTRA_CA_CERTS."); + } return kj::mv(exception); }), [fetcher = kj::mv(fetcher), jsRequest = kj::mv(jsRequest), urlList = kj::mv(urlList), diff --git a/src/workerd/api/tests/fetch-tls-error-test.js b/src/workerd/api/tests/fetch-tls-error-test.js new file mode 100644 index 00000000000..c98d2a5da2e --- /dev/null +++ b/src/workerd/api/tests/fetch-tls-error-test.js @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import assert from 'node:assert'; + +// Test that TLS errors produce helpful error messages rather than opaque "internal error" +export const tlsCertificateErrorMessage = { + async test(ctrl, env, ctx) { + // Attempt to fetch from a server with an untrusted certificate. + // The test server uses a self-signed cert that is NOT in the trustedCertificates list + // for the internet service, so this should fail with a TLS error. + + try { + await fetch(`https://localhost:${env.UNTRUSTED_TLS_PORT}/`); + assert.fail('Expected fetch to fail due to TLS certificate error'); + } catch (err) { + // Verify we get a helpful error message, not an opaque internal error + assert.ok( + !err.message.includes('internal error; reference ='), + `Expected helpful TLS error message, got opaque internal error: ${err.message}` + ); + + // The error should mention TLS/certificate issues and be actionable + assert.ok( + err.message.includes('TLS') || + err.message.includes('certificate') || + err.message.includes('Network connection failed'), + `Expected error message to be helpful about TLS/certificate issues, got: ${err.message}` + ); + + // Should mention NODE_EXTRA_CA_CERTS as the workaround + assert.ok( + err.message.includes('NODE_EXTRA_CA_CERTS'), + `Expected error message to mention NODE_EXTRA_CA_CERTS workaround, got: ${err.message}` + ); + } + }, +}; diff --git a/src/workerd/api/tests/fetch-tls-error-test.wd-test b/src/workerd/api/tests/fetch-tls-error-test.wd-test new file mode 100644 index 00000000000..75dbc9da355 --- /dev/null +++ b/src/workerd/api/tests/fetch-tls-error-test.wd-test @@ -0,0 +1,34 @@ +# Copyright (c) 2024 Cloudflare, Inc. +# Licensed under the Apache 2.0 license found in the LICENSE file or at: +# https://opensource.org/licenses/Apache-2.0 + +using Workerd = import "/workerd/workerd.capnp"; + +const config :Workerd.Config = ( + services = [ + ( + name = "main", + worker = ( + modules = [ + (name = "worker.js", esModule = embed "fetch-tls-error-test.js") + ], + compatibilityFlags = ["nodejs_compat"], + bindings = [ + (name = "UNTRUSTED_TLS_PORT", fromEnvironment = "UNTRUSTED_TLS_PORT"), + ], + ) + ), + # Internet service WITHOUT the test server's CA in trustedCertificates. + # This simulates the real-world scenario where a certificate is not trusted, + # which should trigger the TLS error handling we added. + ( name = "internet", + network = ( + allow = ["private"], + tlsOptions = ( + # Intentionally NOT including any trusted certificates to trigger TLS error + trustedCertificates = [], + ), + ) + ), + ], +);