Skip to content
This repository was archived by the owner on Sep 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/healthy-beans-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/next-on-pages': patch
---

fix Next.js re-defining global `__import_unsupported`
5 changes: 5 additions & 0 deletions .changeset/rich-dogs-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/next-on-pages': patch
---

fix `AbortController`s being created in the global scope
60 changes: 60 additions & 0 deletions packages/next-on-pages/templates/_worker.js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,66 @@ declare const __ALSes_PROMISE__: Promise<null | {
requestContextAsyncLocalStorage: AsyncLocalStorage<unknown>;
}>;

const originalDefineProperty = Object.defineProperty;

const patchedDefineProperty = (
...args: Parameters<typeof Object.defineProperty<unknown>>
) => {
const target = args[0];
const key = args[1];
// Next.js defined an __import_unsupported global property as non configurable
// with next-on-pages this apps try to re-define this property multiple times,
// so here we patch `defineProperty` to just ignore re-definition of such property
const importUnsupportedKey = '__import_unsupported';
if (key === importUnsupportedKey) {
if (
typeof target === 'object' &&
target !== null &&
importUnsupportedKey in target
) {
return;
}
}
return originalDefineProperty(...args);
};

global.Object.defineProperty =
patchedDefineProperty as typeof global.Object.defineProperty;

global.AbortController = class PatchedAbortController extends AbortController {
constructor() {
try {
super();
} catch (e) {
if (
e instanceof Error &&
e.message.includes('Disallowed operation called within global scope')
) {
// Next.js attempted to create an AbortController in the global scope
// let's return something that looks like an AbortController but with
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not cause other problems? What if that AbortController is actually needed to clean stuff up? Are we just pushing the issue down the road with this fix? Do we know what AbortControllers are being instantiated in the top level scope outside of a request?

Copy link
Member Author

@dario-piotrowicz dario-piotrowicz Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not cause other problems? What if that AbortController is actually needed to clean stuff up? Are we just pushing the issue down the road with this fix?

Yes, that's totally the case, I'm just kicking the can down the road, hoping that this won't cause issues, thinking that we can deal with potential issues caused by this simplified implementation if/when they arise

Given the state of the project and the fact that we're focusing our efforts on the opennext adapter instead I would try not to invest too much time in finding the perfect solution here, just something good enough to unblock people while opennext is still in beta.

I totally understand your concerns though 😕

Do we know what AbortControllers are being instantiated in the top level scope outside of a request?

I am pretty confident that this is due to the fact that (for perf benefits) we dynamically import route functions, in those there are setup steps that create the AbortControllers, these aren't really done outside of the request, but workerd does view them in such a way, this is a workerd limitation that I already in the past flagged to the team: https://github.com/dario-piotrowicz/workerd-dynamic-imports-io-repro

// noop functionalities
return {
signal: {
aborted: false,
reason: null,
onabort: () => {
/* empty */
},
throwIfAborted: () => {
/* empty */
},
} as unknown as AbortSignal,
abort() {
/* empty */
},
};
} else {
throw e;
}
}
}
};

export default {
async fetch(request, env, ctx) {
setupRoutesIsolation();
Expand Down
Loading