Title: 24.19.0: --experimental-import-text breaks loader hooks that own with { type: 'text' } · Issue #65210 · nodejs/node · GitHub
Open Graph Title: 24.19.0: --experimental-import-text breaks loader hooks that own with { type: 'text' } · Issue #65210 · nodejs/node
X Title: 24.19.0: --experimental-import-text breaks loader hooks that own with { type: 'text' } · Issue #65210 · nodejs/node
Description: Version 24.19.0 Platform Version: v24.19.0 (works on v24.18.0) Platform: Darwin 24.6.0 arm64 (macOS 15.7), official nvm binaries; originally hit in node:24-alpine on ECS Fargate Subsystem: esm, module Subsystem loader What steps will rep...
Open Graph Description: Version 24.19.0 Platform Version: v24.19.0 (works on v24.18.0) Platform: Darwin 24.6.0 arm64 (macOS 15.7), official nvm binaries; originally hit in node:24-alpine on ECS Fargate Subsystem: esm, mod...
X Description: Version 24.19.0 Platform Version: v24.19.0 (works on v24.18.0) Platform: Darwin 24.6.0 arm64 (macOS 15.7), official nvm binaries; originally hit in node:24-alpine on ECS Fargate Subsystem: esm, mod...
Opengraph URL: https://github.com/nodejs/node/issues/65210
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"24.19.0: --experimental-import-text breaks loader hooks that own with { type: 'text' }","articleBody":"### Version\n\n24.19.0\n\n### Platform\n\n```text\nVersion: v24.19.0 (works on v24.18.0)\nPlatform: Darwin 24.6.0 arm64 (macOS 15.7), official nvm binaries; originally hit in node:24-alpine on ECS Fargate\nSubsystem: esm, module\n```\n\n### Subsystem\n\nloader\n\n### What steps will reproduce the bug?\n\nThree files:\n\n`asset.txt`\n```\nhello from a text asset\n```\n\n`register-hooks.mjs`\n```js\nimport { registerHooks } from 'node:module';\n\nregisterHooks({\n\tload(url, context, nextLoad) {\n\t\tif (context.importAttributes?.type === 'text') {\n\t\t\t// Use nextLoad purely as a source reader, with a format hint Node does not\n\t\t\t// implement, then return the real module ourselves.\n\t\t\tconst { source } = nextLoad(url, { ...context, format: 'text' });\n\t\t\treturn {\n\t\t\t\tformat: 'module',\n\t\t\t\tsource: `export default ${JSON.stringify(source.toString())}`,\n\t\t\t};\n\t\t}\n\t\treturn nextLoad(url, context);\n\t},\n});\n```\n\n`main.mjs`\n```js\nimport text from './asset.txt' with { type: 'text' };\nconsole.log('loaded via hook:', JSON.stringify(text));\n```\n\nRun:\n```console\n$ node --import ./register-hooks.mjs ./main.mjs\n```\n\nv24.18.0 — works:\n```\nloaded via hook: \"hello from a text asset\\n\"\n```\n\nv24.19.0 — throws `ERR_IMPORT_ATTRIBUTE_UNSUPPORTED` (full output in \"What do you see instead\").\n\nThe same repro written against the deprecated `module.register()` API fails the same way, only through `defaultLoad` instead of `defaultLoadSync`.\n\nSame file tree, no flags, no other changes. `git bisect` isn't needed: the change is https://github.com/nodejs/node/commit/3c1636dabf (#62300, SEMVER-MINOR, first released in v24.19.0). Adding `--experimental-import-text` on v24.19.0 makes the repro pass again on both APIs.\n\n### How often does it reproduce? Is there a required condition?\n\n100% deterministic on v24.19.0 and later (not timing-, load- or platform-dependent)\n\nRequired conditions, all four:\n1. A registered load hook — `module.registerHooks()` or the deprecated `module.register()`; both are affected.\n2. An import carrying `with { type: 'text' }`.\n3. The hook delegating to `nextLoad()` (e.g. to read the file's bytes before transforming it) rather than short-circuiting with its own source. Validation lives in `defaultLoad`/`defaultLoadSync`, at the bottom of the hook chain, so only delegating hooks are hit.\n4. `--experimental-import-text` not set.\n\nIndependent of file extension, of the `format` hint passed to `nextLoad()`, and of whether the hooks are synchronous or asynchronous. Unaffected: hooks that use any other attribute value (`with { type: 'buffer' }` etc.) still take the \"unrecognized type\" path and still work, only the value Node claimed in this PR broke.\n\n### What is the expected behavior? Why is that the expected behavior?\n\nWith `--experimental-import-text` absent, v24.19.0 should behave exactly as v24.18.0 did: `type: 'text'` should reach the `case undefined: break` branch in `validateAttributes`, whose own comment states the intent (\"Ignore attributes for module formats we don't recognize, to allow new formats to be added in the future.\") The attribute should be left for loader hooks to interpret.\n\nThree reasons:\n\n1. A SEMVER-MINOR feature behind an experimental flag should be inert when the flag is off. #62300 is described as purely additive, but its flag-off path is a new hard throw on input that previously worked. If the feature is experimental, the rejection cannot be unconditional.\n2. That tolerance branch is the only mechanism loader hooks have for owning an attribute type. #51128 asked for a real one (a hook for custom attributes, or a way to disable built-in validation when hooks are registered) and was closed without providing it. Hooks were left with the ignore-unknown-types path and are now being broken for having used it.\n3. Reading source via `nextLoad()` is the documented way for a load hook to get a file's contents. The break specifically punishes hooks that follow that guidance; short-circuiting hooks that read the file themselves are unaffected.\n\n### What do you see instead?\n\n`registerHooks()` on v24.19.0:\n\n```\nnode:internal/modules/esm/assert:73\n throw new ERR_IMPORT_ATTRIBUTE_UNSUPPORTED('type', importAttributes.type, url);\n ^\n\nTypeError [ERR_IMPORT_ATTRIBUTE_UNSUPPORTED]: Import attribute \"type\" with value \"text\" is not supported in file:///private/tmp/repro-62300/asset.txt\n at validateAttributes (node:internal/modules/esm/assert:73:11)\n at defaultLoadSync (node:internal/modules/esm/load:163:3)\n at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:802:12)\n at nextStep (node:internal/modules/customization_hooks:189:26)\n at load (file:///private/tmp/repro-62300/register-hooks.mjs:8:23)\n at nextStep (node:internal/modules/customization_hooks:189:26)\n at loadWithHooks (node:internal/modules/customization_hooks:374:18)\n at #loadSync (node:internal/modules/esm/loader:825:16)\n at ModuleLoader.load (node:internal/modules/esm/loader:783:26)\n at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:494:31) {\n code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED'\n}\n\nNode.js v24.19.0\n```\n\nWith the deprecated `module.register()`, the same error arrives via `defaultLoad (node:internal/modules/esm/load:108:3)`.\n\nThe throw happens inside the hook's own `nextLoad()` call, so the hook never gets to return. The message names the asset URL and the attribute, but says nothing about the new flag or about the registered hooks, so from an application's point of view the import simply stops working after a patch-level base-image bump.\n\n### Additional information\n\nRoot cause, in `lib/internal/modules/esm/assert.js` as of #62300:\n\n- `formatTypeMap` gained `'text': 'text'`, so `validType` is no longer `undefined` for\n the text format, and\n- a new unconditional check throws before the switch:\n\n ```js\n if (validType !== undefined &&\n importAttributes.type === 'text' &&\n !getOptionValue('--experimental-import-text')) {\n throw new ERR_IMPORT_ATTRIBUTE_UNSUPPORTED('type', importAttributes.type, url);\n }\n ```\n\nSetting `--experimental-import-text` does make the repro pass, so the flag is a workaround — but it is the wrong requirement: an application has to opt into an experimental module feature it does not use, on every entrypoint (`node`, test runners, workers, tooling), purely to stop Node from rejecting an attribute its own loader handles. It is also unavailable on every release line that predates the flag, so there is no single spelling that runs on 24.18 and 24.19.\n\nReal-world impact: a service that loads ~33 email-template asset sets through `with { type: 'text' }` and a registered load hook went from healthy on 24.18 to crash-looping on the first template on 24.19, dying before any application code ran. Because the image is `node:24-alpine`, this arrived with a rebuild rather than a version bump, one day after 24.19.0 was released, and cost a production rollback. Mitigation was to pin the base image, then delete the import-attribute loaders entirely and read the assets with `fs` + `new URL(..., import.meta.url)` across 47 files.\n\nRequests:\n1. Make the flag-off path inert — when `--experimental-import-text` is not set, do not add `'text'` to the recognized-format map and do not throw; fall through to the unrecognized-attribute path so hooks keep working as on 24.18.\n2. Give hooks a supported way to claim attribute types (#51128) before Node claims further type names, otherwise each new built-in type is another silent break for whoever already used that name.\n3. Note the behaviour change for loader-hook consumers in the changelog, and add regression tests covering a registered load hook that handles `with { type: 'text' }` without the flag, for both `module.registerHooks()` (`defaultLoadSync`) and `module.register()` (`defaultLoad`). The PR's tests only cover flag-on and flag-off without hooks.","author":{"url":"https://github.com/richterdennis","@type":"Person","name":"richterdennis"},"datePublished":"2026-08-11T07:26:37Z","url":"https://github.com/nodejs/node/issues/65210"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:eab9c34e-aac8-08db-9302-1ad780445c90 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 836A:D1DAA:626780:85A73D:6AED2512 |
| html-safe-nonce | b5efb4ef22cfbf1b1b13fc4b7e0dd4e5182564b35e0ff13b4985740d6605cccb |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4MzZBOkQxREFBOjYyNjc4MDo4NUE3M0Q6NkFFRDI1MTIiLCJ2aXNpdG9yX2lkIjoiMjMxMzM0NDY5MTA4MTEyNzE4NiIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 9d0024539eaeeee306771a4813442c096377cc56a34719baac91f6da1f7faf52 |
| hovercard-subject-tag | issue:5117753781 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/65210/issue_layout |
| twitter:image | https://opengraph.githubassets.com/c9dfb36968b2e728ad1081b1850a069b9b9464875509791c17021bb2bae1f430/nodejs/node/issues/65210 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/c9dfb36968b2e728ad1081b1850a069b9b9464875509791c17021bb2bae1f430/nodejs/node/issues/65210 |
| og:image:alt | Version 24.19.0 Platform Version: v24.19.0 (works on v24.18.0) Platform: Darwin 24.6.0 arm64 (macOS 15.7), official nvm binaries; originally hit in node:24-alpine on ECS Fargate Subsystem: esm, mod... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | richterdennis |
| hostname | github.com |
| expected-hostname | github.com |
| None | e3e0f7f63e50c6fe879198a93a0a023a648ecc0a5e9bc3b5e033ac57172237e6 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 707a24b346ac8a82878b9c084efaa38a997660cb |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width