@vercel/python 5.0.6 → 5.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +155 -44
  2. package/package.json +2 -2
  3. package/vc_init.py +148 -76
package/dist/index.js CHANGED
@@ -2836,16 +2836,10 @@ async function getUserScriptsDir(pythonPath) {
2836
2836
  return null;
2837
2837
  }
2838
2838
  }
2839
- async function pipInstall(pipPath, pythonPath, workPath, args, targetDir) {
2839
+ async function pipInstall(pipPath, uvPath, workPath, args, targetDir) {
2840
2840
  const target = targetDir ? (0, import_path.join)(targetDir, resolveVendorDir()) : resolveVendorDir();
2841
2841
  process.env.PIP_USER = "0";
2842
- let uvBin = null;
2843
- try {
2844
- uvBin = await getUvBinaryOrInstall(pythonPath);
2845
- } catch (err) {
2846
- console.log("Failed to install uv, falling back to pip");
2847
- }
2848
- if (uvBin) {
2842
+ if (uvPath) {
2849
2843
  const uvArgs = [
2850
2844
  "pip",
2851
2845
  "install",
@@ -2855,10 +2849,10 @@ async function pipInstall(pipPath, pythonPath, workPath, args, targetDir) {
2855
2849
  target,
2856
2850
  ...args
2857
2851
  ];
2858
- const prettyUv = `${uvBin} ${uvArgs.join(" ")}`;
2852
+ const prettyUv = `${uvPath} ${uvArgs.join(" ")}`;
2859
2853
  (0, import_build_utils.debug)(`Running "${prettyUv}"...`);
2860
2854
  try {
2861
- await (0, import_execa.default)(uvBin, uvArgs, {
2855
+ await (0, import_execa.default)(uvPath, uvArgs, {
2862
2856
  cwd: workPath
2863
2857
  });
2864
2858
  return;
@@ -2932,10 +2926,8 @@ async function maybeFindUvBin(pythonPath) {
2932
2926
  }
2933
2927
  async function getUvBinaryOrInstall(pythonPath) {
2934
2928
  const uvBin = await maybeFindUvBin(pythonPath);
2935
- if (uvBin) {
2936
- console.log(`Using uv at "${uvBin}"`);
2929
+ if (uvBin)
2937
2930
  return uvBin;
2938
- }
2939
2931
  try {
2940
2932
  console.log("Installing uv...");
2941
2933
  await (0, import_execa.default)(
@@ -2966,6 +2958,7 @@ async function getUvBinaryOrInstall(pythonPath) {
2966
2958
  async function installRequirement({
2967
2959
  pythonPath,
2968
2960
  pipPath,
2961
+ uvPath,
2969
2962
  dependency,
2970
2963
  version: version2,
2971
2964
  workPath,
@@ -2981,11 +2974,12 @@ async function installRequirement({
2981
2974
  return;
2982
2975
  }
2983
2976
  const exact = `${dependency}==${version2}`;
2984
- await pipInstall(pipPath, pythonPath, workPath, [exact, ...args], targetDir);
2977
+ await pipInstall(pipPath, uvPath, workPath, [exact, ...args], targetDir);
2985
2978
  }
2986
2979
  async function installRequirementsFile({
2987
2980
  pythonPath,
2988
2981
  pipPath,
2982
+ uvPath,
2989
2983
  filePath,
2990
2984
  workPath,
2991
2985
  targetDir,
@@ -2999,27 +2993,29 @@ async function installRequirementsFile({
2999
2993
  }
3000
2994
  await pipInstall(
3001
2995
  pipPath,
3002
- pythonPath,
2996
+ uvPath,
3003
2997
  workPath,
3004
2998
  ["--upgrade", "-r", filePath, ...args],
3005
2999
  targetDir
3006
3000
  );
3007
3001
  }
3008
- async function exportRequirementsFromUv(pythonPath, projectDir, options = {}) {
3002
+ async function exportRequirementsFromUv(projectDir, uvPath, options = {}) {
3009
3003
  const { locked = false } = options;
3010
- const uvBin = await getUvBinaryOrInstall(pythonPath);
3004
+ if (!uvPath) {
3005
+ throw new Error("uv is not available to export requirements");
3006
+ }
3011
3007
  const args = ["export"];
3012
3008
  if (locked) {
3013
3009
  args.push("--frozen");
3014
3010
  }
3015
- (0, import_build_utils.debug)(`Running "${uvBin} ${args.join(" ")}" in ${projectDir}...`);
3011
+ (0, import_build_utils.debug)(`Running "${uvPath} ${args.join(" ")}" in ${projectDir}...`);
3016
3012
  let stdout;
3017
3013
  try {
3018
- const { stdout: out } = await (0, import_execa.default)(uvBin, args, { cwd: projectDir });
3014
+ const { stdout: out } = await (0, import_execa.default)(uvPath, args, { cwd: projectDir });
3019
3015
  stdout = out;
3020
3016
  } catch (err) {
3021
3017
  throw new Error(
3022
- `Failed to run "${uvBin} ${args.join(" ")}": ${err instanceof Error ? err.message : String(err)}`
3018
+ `Failed to run "${uvPath} ${args.join(" ")}": ${err instanceof Error ? err.message : String(err)}`
3023
3019
  );
3024
3020
  }
3025
3021
  const tmpDir = await import_fs.default.promises.mkdtemp((0, import_path.join)(import_os.default.tmpdir(), "vercel-uv-"));
@@ -3031,6 +3027,7 @@ async function exportRequirementsFromUv(pythonPath, projectDir, options = {}) {
3031
3027
  async function exportRequirementsFromPipfile({
3032
3028
  pythonPath,
3033
3029
  pipPath,
3030
+ uvPath,
3034
3031
  projectDir,
3035
3032
  meta
3036
3033
  }) {
@@ -3044,7 +3041,8 @@ async function exportRequirementsFromPipfile({
3044
3041
  version: "0.3.0",
3045
3042
  workPath: tempDir,
3046
3043
  meta,
3047
- args: ["--no-warn-script-location"]
3044
+ args: ["--no-warn-script-location"],
3045
+ uvPath
3048
3046
  });
3049
3047
  const tempVendorDir = (0, import_path.join)(tempDir, resolveVendorDir());
3050
3048
  const convertCmd = isWin ? (0, import_path.join)(tempVendorDir, "Scripts", "pipfile2req.exe") : (0, import_path.join)(tempVendorDir, "bin", "pipfile2req");
@@ -3151,16 +3149,27 @@ function getSupportedPythonVersion({
3151
3149
  }
3152
3150
  if (isInstalled2(requested)) {
3153
3151
  selection = requested;
3152
+ console.log(`Using Python ${selection.version} from ${source}`);
3154
3153
  } else {
3155
3154
  console.warn(
3156
3155
  `Warning: Python version "${version2}" detected in ${source} is not installed and will be ignored. http://vercel.link/python-version`
3157
3156
  );
3157
+ console.log(
3158
+ `Falling back to latest installed version: ${selection.version}`
3159
+ );
3158
3160
  }
3159
3161
  } else {
3160
3162
  console.warn(
3161
3163
  `Warning: Python version "${version2}" detected in ${source} is invalid and will be ignored. http://vercel.link/python-version`
3162
3164
  );
3165
+ console.log(
3166
+ `Falling back to latest installed version: ${selection.version}`
3167
+ );
3163
3168
  }
3169
+ } else {
3170
+ console.log(
3171
+ `No Python version specified in pyproject.toml or Pipfile.lock. Using latest installed version: ${selection.version}`
3172
+ );
3164
3173
  }
3165
3174
  if (isDiscontinued(selection)) {
3166
3175
  throw new import_build_utils2.NowBuildError({
@@ -3215,6 +3224,47 @@ function isFastapiEntrypoint(file) {
3215
3224
  return false;
3216
3225
  }
3217
3226
  }
3227
+ var FLASK_ENTRYPOINT_FILENAMES = ["app", "index", "server", "main"];
3228
+ var FLASK_ENTRYPOINT_DIRS = ["", "src", "app"];
3229
+ var FLASK_CONTENT_REGEX = /(from\s+flask\s+import\s+Flask|import\s+flask|Flask\s*\()/;
3230
+ var FLASK_CANDIDATE_ENTRYPOINTS = FLASK_ENTRYPOINT_FILENAMES.flatMap(
3231
+ (filename) => FLASK_ENTRYPOINT_DIRS.map(
3232
+ (dir) => import_path2.posix.join(dir, `${filename}.py`)
3233
+ )
3234
+ );
3235
+ function isFlaskEntrypoint(file) {
3236
+ try {
3237
+ const fsPath = file.fsPath;
3238
+ if (!fsPath)
3239
+ return false;
3240
+ const contents = import_fs2.default.readFileSync(fsPath, "utf8");
3241
+ return FLASK_CONTENT_REGEX.test(contents);
3242
+ } catch {
3243
+ return false;
3244
+ }
3245
+ }
3246
+ async function detectFlaskEntrypoint(workPath, configuredEntrypoint) {
3247
+ const entry = configuredEntrypoint.endsWith(".py") ? configuredEntrypoint : `${configuredEntrypoint}.py`;
3248
+ try {
3249
+ const fsFiles = await (0, import_build_utils3.glob)("**", workPath);
3250
+ if (fsFiles[entry])
3251
+ return entry;
3252
+ const candidates = FLASK_CANDIDATE_ENTRYPOINTS.filter(
3253
+ (c) => !!fsFiles[c]
3254
+ );
3255
+ if (candidates.length > 0) {
3256
+ const flaskEntrypoint = candidates.find(
3257
+ (c) => isFlaskEntrypoint(fsFiles[c])
3258
+ ) || candidates[0];
3259
+ (0, import_build_utils3.debug)(`Detected Flask entrypoint: ${flaskEntrypoint}`);
3260
+ return flaskEntrypoint;
3261
+ }
3262
+ return null;
3263
+ } catch {
3264
+ (0, import_build_utils3.debug)("Failed to discover entrypoint for Flask");
3265
+ return null;
3266
+ }
3267
+ }
3218
3268
  async function detectFastapiEntrypoint(workPath, configuredEntrypoint) {
3219
3269
  const entry = configuredEntrypoint.endsWith(".py") ? configuredEntrypoint : `${configuredEntrypoint}.py`;
3220
3270
  try {
@@ -3606,7 +3656,6 @@ var build = async ({
3606
3656
  meta = {},
3607
3657
  config
3608
3658
  }) => {
3609
- let pythonVersion = getLatestPythonVersion(meta);
3610
3659
  workPath = await downloadFilesInWorkPath({
3611
3660
  workPath,
3612
3661
  files: originalFiles,
@@ -3637,6 +3686,20 @@ var build = async ({
3637
3686
  message: `No FastAPI entrypoint found. Searched for: ${searchedList}`
3638
3687
  });
3639
3688
  }
3689
+ } else if (!fsFiles[entrypoint] && config?.framework === "flask") {
3690
+ const detected = await detectFlaskEntrypoint(workPath, entrypoint);
3691
+ if (detected) {
3692
+ (0, import_build_utils5.debug)(
3693
+ `Resolved Python entrypoint to "${detected}" (configured "${entrypoint}" not found).`
3694
+ );
3695
+ entrypoint = detected;
3696
+ } else {
3697
+ const searchedList = FLASK_CANDIDATE_ENTRYPOINTS.join(", ");
3698
+ throw new import_build_utils5.NowBuildError({
3699
+ code: "FLASK_ENTRYPOINT_NOT_FOUND",
3700
+ message: `No Flask entrypoint found. Searched for: ${searchedList}`
3701
+ });
3702
+ }
3640
3703
  }
3641
3704
  const entryDirectory = (0, import_path5.dirname)(entrypoint);
3642
3705
  const hasReqLocal = !!fsFiles[(0, import_path5.join)(entryDirectory, "requirements.txt")];
@@ -3655,22 +3718,26 @@ var build = async ({
3655
3718
  });
3656
3719
  const pipfileLockDir = fsFiles[(0, import_path5.join)(entryDirectory, "Pipfile.lock")] ? (0, import_path5.join)(workPath, entryDirectory) : fsFiles["Pipfile.lock"] ? workPath : null;
3657
3720
  const pipfileDir = fsFiles[(0, import_path5.join)(entryDirectory, "Pipfile")] ? (0, import_path5.join)(workPath, entryDirectory) : fsFiles["Pipfile"] ? workPath : null;
3721
+ let declaredPythonVersion;
3658
3722
  if (pyprojectDir) {
3659
3723
  let requiresPython;
3660
3724
  try {
3661
3725
  const pyproject = await (0, import_build_utils6.readConfigFile)((0, import_path5.join)(pyprojectDir, "pyproject.toml"));
3662
3726
  requiresPython = pyproject?.project?.["requires-python"];
3663
- } catch {
3664
- (0, import_build_utils5.debug)("Failed to parse pyproject.toml");
3727
+ } catch (err) {
3728
+ (0, import_build_utils5.debug)("Failed to parse pyproject.toml", err);
3665
3729
  }
3666
3730
  const VERSION_REGEX = /\b\d+\.\d+\b/;
3667
3731
  const exact = requiresPython?.trim().match(VERSION_REGEX)?.[0];
3668
3732
  if (exact) {
3669
- const selected = getSupportedPythonVersion({
3670
- isDev: meta.isDev,
3671
- declaredPythonVersion: { version: exact, source: "pyproject.toml" }
3672
- });
3673
- pythonVersion = selected;
3733
+ declaredPythonVersion = { version: exact, source: "pyproject.toml" };
3734
+ (0, import_build_utils5.debug)(
3735
+ `Found Python version ${exact} in pyproject.toml (requires-python: "${requiresPython}")`
3736
+ );
3737
+ } else if (requiresPython) {
3738
+ (0, import_build_utils5.debug)(
3739
+ `Could not parse Python version from pyproject.toml requires-python: "${requiresPython}"`
3740
+ );
3674
3741
  }
3675
3742
  } else if (pipfileLockDir) {
3676
3743
  let lock = {};
@@ -3684,11 +3751,15 @@ var build = async ({
3684
3751
  });
3685
3752
  }
3686
3753
  const pyFromLock = lock?._meta?.requires?.python_version;
3687
- pythonVersion = getSupportedPythonVersion({
3688
- isDev: meta.isDev,
3689
- declaredPythonVersion: pyFromLock ? { version: pyFromLock, source: "Pipfile.lock" } : void 0
3690
- });
3754
+ if (pyFromLock) {
3755
+ declaredPythonVersion = { version: pyFromLock, source: "Pipfile.lock" };
3756
+ (0, import_build_utils5.debug)(`Found Python version ${pyFromLock} in Pipfile.lock`);
3757
+ }
3691
3758
  }
3759
+ const pythonVersion = getSupportedPythonVersion({
3760
+ isDev: meta.isDev,
3761
+ declaredPythonVersion
3762
+ });
3692
3763
  fsFiles = await (0, import_build_utils5.glob)("**", workPath);
3693
3764
  const requirementsTxt = (0, import_path5.join)(entryDirectory, "requirements.txt");
3694
3765
  const vendorBaseDir = (0, import_path5.join)(
@@ -3704,10 +3775,42 @@ var build = async ({
3704
3775
  console.log("Failed to create vendor cache directory");
3705
3776
  throw err;
3706
3777
  }
3707
- console.log("Installing required dependencies...");
3778
+ let installationSource;
3779
+ if (uvLockDir && pyprojectDir) {
3780
+ installationSource = "uv.lock";
3781
+ } else if (pyprojectDir) {
3782
+ installationSource = "pyproject.toml";
3783
+ } else if (pipfileLockDir) {
3784
+ installationSource = "Pipfile.lock";
3785
+ } else if (pipfileDir) {
3786
+ installationSource = "Pipfile";
3787
+ } else if (fsFiles[requirementsTxt] || fsFiles["requirements.txt"]) {
3788
+ installationSource = "requirements.txt";
3789
+ }
3790
+ if (installationSource) {
3791
+ console.log(
3792
+ `Installing required dependencies from ${installationSource}...`
3793
+ );
3794
+ } else {
3795
+ console.log("Installing required dependencies...");
3796
+ }
3797
+ let uvPath = null;
3798
+ try {
3799
+ uvPath = await getUvBinaryOrInstall(pythonVersion.pythonPath);
3800
+ console.log(`Using uv at "${uvPath}"`);
3801
+ } catch (err) {
3802
+ if (uvLockDir || pyprojectDir && !hasReqLocal && !hasReqGlobal) {
3803
+ console.log("Failed to install uv");
3804
+ throw new Error(
3805
+ `uv is required for this project but failed to install: ${err instanceof Error ? err.message : String(err)}`
3806
+ );
3807
+ }
3808
+ (0, import_build_utils5.debug)("Failed to install uv", err);
3809
+ }
3708
3810
  await installRequirement({
3709
3811
  pythonPath: pythonVersion.pythonPath,
3710
3812
  pipPath: pythonVersion.pipPath,
3813
+ uvPath,
3711
3814
  dependency: "werkzeug",
3712
3815
  version: "1.0.1",
3713
3816
  workPath,
@@ -3718,14 +3821,13 @@ var build = async ({
3718
3821
  if (uvLockDir) {
3719
3822
  (0, import_build_utils5.debug)('Found "uv.lock"');
3720
3823
  if (pyprojectDir) {
3721
- const exportedReq = await exportRequirementsFromUv(
3722
- pythonVersion.pythonPath,
3723
- pyprojectDir,
3724
- { locked: true }
3725
- );
3824
+ const exportedReq = await exportRequirementsFromUv(pyprojectDir, uvPath, {
3825
+ locked: true
3826
+ });
3726
3827
  await installRequirementsFile({
3727
3828
  pythonPath: pythonVersion.pythonPath,
3728
3829
  pipPath: pythonVersion.pipPath,
3830
+ uvPath,
3729
3831
  filePath: exportedReq,
3730
3832
  workPath,
3731
3833
  targetDir: vendorBaseDir,
@@ -3742,14 +3844,13 @@ var build = async ({
3742
3844
  "Detected both pyproject.toml and requirements.txt but no lockfile; using pyproject.toml"
3743
3845
  );
3744
3846
  }
3745
- const exportedReq = await exportRequirementsFromUv(
3746
- pythonVersion.pythonPath,
3747
- pyprojectDir,
3748
- { locked: false }
3749
- );
3847
+ const exportedReq = await exportRequirementsFromUv(pyprojectDir, uvPath, {
3848
+ locked: false
3849
+ });
3750
3850
  await installRequirementsFile({
3751
3851
  pythonPath: pythonVersion.pythonPath,
3752
3852
  pipPath: pythonVersion.pipPath,
3853
+ uvPath,
3753
3854
  filePath: exportedReq,
3754
3855
  workPath,
3755
3856
  targetDir: vendorBaseDir,
@@ -3764,12 +3865,14 @@ var build = async ({
3764
3865
  const exportedReq = await exportRequirementsFromPipfile({
3765
3866
  pythonPath: pythonVersion.pythonPath,
3766
3867
  pipPath: pythonVersion.pipPath,
3868
+ uvPath,
3767
3869
  projectDir: pipfileLockDir || pipfileDir,
3768
3870
  meta
3769
3871
  });
3770
3872
  await installRequirementsFile({
3771
3873
  pythonPath: pythonVersion.pythonPath,
3772
3874
  pipPath: pythonVersion.pipPath,
3875
+ uvPath,
3773
3876
  filePath: exportedReq,
3774
3877
  workPath,
3775
3878
  targetDir: vendorBaseDir,
@@ -3784,6 +3887,7 @@ var build = async ({
3784
3887
  await installRequirementsFile({
3785
3888
  pythonPath: pythonVersion.pythonPath,
3786
3889
  pipPath: pythonVersion.pipPath,
3890
+ uvPath,
3787
3891
  filePath: requirementsTxtPath,
3788
3892
  workPath,
3789
3893
  targetDir: vendorBaseDir,
@@ -3795,6 +3899,7 @@ var build = async ({
3795
3899
  await installRequirementsFile({
3796
3900
  pythonPath: pythonVersion.pythonPath,
3797
3901
  pipPath: pythonVersion.pipPath,
3902
+ uvPath,
3798
3903
  filePath: requirementsTxtPath,
3799
3904
  workPath,
3800
3905
  targetDir: vendorBaseDir,
@@ -3865,6 +3970,12 @@ var shouldServe = (opts) => {
3865
3970
  return false;
3866
3971
  }
3867
3972
  return true;
3973
+ } else if (framework === "flask") {
3974
+ const requestPath = opts.requestPath.replace(/\/$/, "");
3975
+ if (requestPath.startsWith("api") && opts.hasMatched) {
3976
+ return false;
3977
+ }
3978
+ return true;
3868
3979
  }
3869
3980
  return defaultShouldServe(opts);
3870
3981
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "5.0.6",
3
+ "version": "5.0.8",
4
4
  "main": "./dist/index.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -20,7 +20,7 @@
20
20
  "@types/jest": "27.4.1",
21
21
  "@types/node": "14.18.33",
22
22
  "@types/which": "3.0.0",
23
- "@vercel/build-utils": "12.1.0",
23
+ "@vercel/build-utils": "12.1.2",
24
24
  "cross-env": "7.0.3",
25
25
  "execa": "^1.0.0",
26
26
  "fs-extra": "11.1.1",
package/vc_init.py CHANGED
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import sys
2
3
  import os
3
4
  import site
@@ -7,9 +8,17 @@ import json
7
8
  import inspect
8
9
  import threading
9
10
  import asyncio
11
+ import http
12
+ import time
10
13
  from importlib import util
11
- from http.server import BaseHTTPRequestHandler
14
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
12
15
  import socket
16
+ import functools
17
+ import logging
18
+ import builtins
19
+ from typing import Callable, Literal
20
+ import contextvars
21
+ import io
13
22
 
14
23
  _here = os.path.dirname(__file__)
15
24
  _vendor_rel = '__VC_HANDLER_VENDOR_DIR'
@@ -53,72 +62,69 @@ def format_headers(headers, decode=False):
53
62
  keyToList[key].append(value)
54
63
  return keyToList
55
64
 
56
- if 'VERCEL_IPC_PATH' in os.environ:
57
- from http.server import ThreadingHTTPServer
58
- import http
59
- import time
60
- import contextvars
61
- import functools
62
- import builtins
63
- import logging
65
+ # Custom logging handler so logs are properly categorized
66
+ class VCLogHandler(logging.Handler):
67
+ def __init__(self, send_message: Callable[[dict], None], context_getter: Callable[[], dict] | None = None):
68
+ super().__init__()
69
+ self._send_message = send_message
70
+ self._context_getter = context_getter
64
71
 
65
- start_time = time.time()
66
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
67
- sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
72
+ def emit(self, record):
73
+ try:
74
+ message = record.getMessage()
75
+ except Exception:
76
+ try:
77
+ message = f"{record.msg}"
78
+ except Exception:
79
+ message = ""
80
+
81
+ if record.levelno >= logging.CRITICAL:
82
+ level = "fatal"
83
+ elif record.levelno >= logging.ERROR:
84
+ level = "error"
85
+ elif record.levelno >= logging.WARNING:
86
+ level = "warn"
87
+ elif record.levelno >= logging.INFO:
88
+ level = "info"
89
+ else:
90
+ level = "debug"
68
91
 
69
- send_message = lambda message: sock.sendall((json.dumps(message) + '\0').encode())
70
- storage = contextvars.ContextVar('storage', default=None)
92
+ ctx = None
93
+ try:
94
+ ctx = self._context_getter() if self._context_getter is not None else None
95
+ except Exception:
96
+ ctx = None
71
97
 
72
- # Override urlopen from urllib3 (& requests) to send Request Metrics
73
- try:
74
- import urllib3
75
- from urllib.parse import urlparse
98
+ if ctx is not None:
99
+ try:
100
+ self._send_message({
101
+ "type": "log",
102
+ "payload": {
103
+ "context": {
104
+ "invocationId": ctx['invocationId'],
105
+ "requestId": ctx['requestId'],
106
+ },
107
+ "message": base64.b64encode(message.encode()).decode(),
108
+ "level": level,
109
+ }
110
+ })
111
+ except Exception:
112
+ pass
113
+ else:
114
+ try:
115
+ sys.stdout.write(message + "\n")
116
+ except Exception:
117
+ pass
76
118
 
77
- def timed_request(func):
78
- fetchId = 0
79
- @functools.wraps(func)
80
- def wrapper(self, method, url, *args, **kwargs):
81
- nonlocal fetchId
82
- fetchId += 1
83
- start_time = int(time.time() * 1000)
84
- result = func(self, method, url, *args, **kwargs)
85
- elapsed_time = int(time.time() * 1000) - start_time
86
- parsed_url = urlparse(url)
87
- context = storage.get()
88
- if context is not None:
89
- send_message({
90
- "type": "metric",
91
- "payload": {
92
- "context": {
93
- "invocationId": context['invocationId'],
94
- "requestId": context['requestId'],
95
- },
96
- "type": "fetch-metric",
97
- "payload": {
98
- "pathname": parsed_url.path,
99
- "search": parsed_url.query,
100
- "start": start_time,
101
- "duration": elapsed_time,
102
- "host": parsed_url.hostname or self.host,
103
- "statusCode": result.status,
104
- "method": method,
105
- "id": fetchId
106
- }
107
- }
108
- })
109
- return result
110
- return wrapper
111
- urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
112
- except:
113
- pass
114
119
 
120
+ def setup_logging(send_message: Callable[[dict], None], storage: contextvars.ContextVar[dict | None]):
115
121
  # Override sys.stdout and sys.stderr to map logs to the correct request
116
122
  class StreamWrapper:
117
- def __init__(self, stream, stream_name):
123
+ def __init__(self, stream: io.TextIOBase, stream_name: Literal["stdout", "stderr"]):
118
124
  self.stream = stream
119
125
  self.stream_name = stream_name
120
126
 
121
- def write(self, message):
127
+ def write(self, message: str):
122
128
  context = storage.get()
123
129
  if context is not None:
124
130
  send_message({
@@ -141,19 +147,15 @@ if 'VERCEL_IPC_PATH' in os.environ:
141
147
  sys.stdout = StreamWrapper(sys.stdout, "stdout")
142
148
  sys.stderr = StreamWrapper(sys.stderr, "stderr")
143
149
 
144
- # Override the global print to log to stdout
145
- def print_wrapper(func):
150
+ # Wrap top-level logging helpers to emit structured logs when a request
151
+ # context is available; otherwise fall back to the original behavior.
152
+ def logging_wrapper(func: Callable[..., None], level: str = "info") -> Callable[..., None]:
146
153
  @functools.wraps(func)
147
154
  def wrapper(*args, **kwargs):
148
- sys.stdout.write(' '.join(map(str, args)) + '\n')
149
- return wrapper
150
- builtins.print = print_wrapper(builtins.print)
151
-
152
- # Override logging to maps logs to the correct request
153
- def logging_wrapper(func, level="info"):
154
- @functools.wraps(func)
155
- def wrapper(*args, **kwargs):
156
- context = storage.get()
155
+ try:
156
+ context = storage.get()
157
+ except Exception:
158
+ context = None
157
159
  if context is not None:
158
160
  send_message({
159
161
  "type": "log",
@@ -170,12 +172,77 @@ if 'VERCEL_IPC_PATH' in os.environ:
170
172
  func(*args, **kwargs)
171
173
  return wrapper
172
174
 
173
- logging.basicConfig(level=logging.INFO)
174
- logging.debug = logging_wrapper(logging.debug)
175
- logging.info = logging_wrapper(logging.info)
175
+ logging.basicConfig(level=logging.INFO, handlers=[VCLogHandler(send_message, storage.get)], force=True)
176
+ logging.debug = logging_wrapper(logging.debug, "debug")
177
+ logging.info = logging_wrapper(logging.info, "info")
176
178
  logging.warning = logging_wrapper(logging.warning, "warn")
177
179
  logging.error = logging_wrapper(logging.error, "error")
178
- logging.critical = logging_wrapper(logging.critical, "error")
180
+ logging.fatal = logging_wrapper(logging.fatal, "fatal")
181
+ logging.critical = logging_wrapper(logging.critical, "fatal")
182
+
183
+ # Ensure built-in print funnels through stdout wrapper so prints are
184
+ # attributed to the current request context.
185
+ def print_wrapper(func: Callable[..., None]) -> Callable[..., None]:
186
+ @functools.wraps(func)
187
+ def wrapper(*args, **kwargs):
188
+ sys.stdout.write(' '.join(map(str, args)) + '\n')
189
+ return wrapper
190
+
191
+ builtins.print = print_wrapper(builtins.print)
192
+
193
+
194
+ if 'VERCEL_IPC_PATH' in os.environ:
195
+ start_time = time.time()
196
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
197
+ sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
198
+
199
+ send_message = lambda message: sock.sendall((json.dumps(message) + '\0').encode())
200
+ storage = contextvars.ContextVar('storage', default=None)
201
+
202
+ # Override urlopen from urllib3 (& requests) to send Request Metrics
203
+ try:
204
+ import urllib3
205
+ from urllib.parse import urlparse
206
+
207
+ def timed_request(func):
208
+ fetchId = 0
209
+ @functools.wraps(func)
210
+ def wrapper(self, method, url, *args, **kwargs):
211
+ nonlocal fetchId
212
+ fetchId += 1
213
+ start_time = int(time.time() * 1000)
214
+ result = func(self, method, url, *args, **kwargs)
215
+ elapsed_time = int(time.time() * 1000) - start_time
216
+ parsed_url = urlparse(url)
217
+ context = storage.get()
218
+ if context is not None:
219
+ send_message({
220
+ "type": "metric",
221
+ "payload": {
222
+ "context": {
223
+ "invocationId": context['invocationId'],
224
+ "requestId": context['requestId'],
225
+ },
226
+ "type": "fetch-metric",
227
+ "payload": {
228
+ "pathname": parsed_url.path,
229
+ "search": parsed_url.query,
230
+ "start": start_time,
231
+ "duration": elapsed_time,
232
+ "host": parsed_url.hostname or self.host,
233
+ "statusCode": result.status,
234
+ "method": method,
235
+ "id": fetchId
236
+ }
237
+ }
238
+ })
239
+ return result
240
+ return wrapper
241
+ urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
242
+ except:
243
+ pass
244
+
245
+ setup_logging(send_message, storage)
179
246
 
180
247
  class BaseHandler(BaseHTTPRequestHandler):
181
248
  # Re-implementation of BaseHTTPRequestHandler's log_message method to
@@ -367,6 +434,8 @@ if 'VERCEL_IPC_PATH' in os.environ:
367
434
 
368
435
  # Event to signal that the response has been fully sent
369
436
  response_done = threading.Event()
437
+ # Event to signal the ASGI app has fully completed (incl. background tasks)
438
+ app_done = threading.Event()
370
439
 
371
440
  # Propagate request context to background thread for logging & metrics
372
441
  request_context = storage.get()
@@ -421,6 +490,8 @@ if 'VERCEL_IPC_PATH' in os.environ:
421
490
  # Run ASGI app (includes background tasks)
422
491
  asgi_instance = app(scope, receive, send)
423
492
  await asgi_instance
493
+ # Mark app completion when the ASGI callable returns
494
+ app_done.set()
424
495
 
425
496
  asyncio.run(runner())
426
497
  except Exception:
@@ -437,10 +508,9 @@ if 'VERCEL_IPC_PATH' in os.environ:
437
508
  pass
438
509
  finally:
439
510
  # Always unblock the waiting thread to avoid hangs
440
- try:
441
- response_done.set()
442
- except Exception:
443
- pass
511
+ response_done.set()
512
+ # Ensure app completion is always signaled
513
+ app_done.set()
444
514
  if token is not None:
445
515
  storage.reset(token)
446
516
 
@@ -450,6 +520,8 @@ if 'VERCEL_IPC_PATH' in os.environ:
450
520
 
451
521
  # Wait until final body chunk has been flushed to client
452
522
  response_done.wait()
523
+ # Also wait until the ASGI app finishes (includes background tasks)
524
+ app_done.wait()
453
525
 
454
526
  if 'Handler' in locals():
455
527
  server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)