Is there a way to abort streaming in nextjs14 while at generateMetadata stage?
Answered
Whiteleg shrimp posted this in #help-forum
Whiteleg shrimpOP
Iam trying to figure out how to use new nextjs with its app router and streaming properly. And a little bit stuck. I have a simple page with a component surrounded by suspense. Component does a request to a throttled endpoint that takes 10 seconds to resolve. Then I'am adding a generateMetadata to the page and call notFound(). Browser immediately shows 404 page with a proper status code - thats okay. And then I expect that client side finishes to do things at that point, but it continues to await that 10s throttled data to stream. I feel that I miss something. Especially when doing curl you need to wait all 10 secs to receive that 404. Is there a way to abort streaming if it is already known at the beginning (metadata) that it will end up with 404 and no more data needs to be sent?
27 Replies
@Whiteleg shrimp Iam trying to figure out how to use new nextjs with its app router and streaming properly. And a little bit stuck. I have a simple page with a component surrounded by suspense. Component does a request to a throttled endpoint that takes 10 seconds to resolve. Then I'am adding a generateMetadata to the page and call notFound(). Browser immediately shows 404 page with a proper status code - thats okay. And then I expect that client side finishes to do things at that point, but it continues to await that 10s throttled data to stream. I feel that I miss something. Especially when doing curl you need to wait all 10 secs to receive that 404. Is there a way to abort streaming if it is already known at the beginning (metadata) that it will end up with 404 and no more data needs to be sent?
try this
const abortController = new AbortController();
export async function generateMetadata() {
abortController.abort();
notFound();
}
export default async function Page() {
const res = await fetch("", {
signal: abortController.signal,
cache: "no-store",
});
return <div></div>;
}@Ray try this
ts
const abortController = new AbortController();
export async function generateMetadata() {
abortController.abort();
notFound();
}
export default async function Page() {
const res = await fetch("", {
signal: abortController.signal,
cache: "no-store",
});
return <div></div>;
}
Whiteleg shrimpOP
thanks fro reply! actually ive tried abort controller, but i made it in singleton module and imported to pass it to every fetch in project, so once it become aborted, none of the further fetches work) In your example I see an idea to create a new controller on a Page level, and then pass it to child components (that actually invoke fetches) with props?
@Whiteleg shrimp thanks fro reply! actually ive tried abort controller, but i made it in singleton module and imported to pass it to every fetch in project, so once it become aborted, none of the further fetches work) In your example I see an idea to create a new controller on a Page level, and then pass it to child components (that actually invoke fetches) with props?
btw, I think you could do this instead?
const findPage = cache(async () => {
return null;
});
export async function generateMetadata() {
const page = await findPage();
if (!page) notFound();
}
export default async function Page() {
const page = await findPage();
if (!page) notFound();
return <div></div>;
}@Ray btw, I think you could do this instead?
ts
const findPage = cache(async () => {
return null;
});
export async function generateMetadata() {
const page = await findPage();
if (!page) notFound();
}
export default async function Page() {
const page = await findPage();
if (!page) notFound();
return <div></div>;
}
Whiteleg shrimpOP
yep, iam actually doing that, but I have not the only one parallel fetch running at a time, and the other fetches wont stop
i hoped there is something like
notFound({ killAll: true })@Ray try this
ts
const abortController = new AbortController();
export async function generateMetadata() {
abortController.abort();
notFound();
}
export default async function Page() {
const res = await fetch("", {
signal: abortController.signal,
cache: "no-store",
});
return <div></div>;
}
oh try this maybe and pass to child
Whiteleg shrimpOP
sadly it wont work because abort controller persists
@Whiteleg shrimp sadly it wont work because abort controller persists
could you show some code on how you doing fetching?
@Ray could you show some code on how you doing fetching?
Whiteleg shrimpOP
gimme a minute i need to figure out how to post that shiny code snippets first
@Whiteleg shrimp gimme a minute i need to figure out how to post that shiny code snippets first
wrap the code with three `
Whiteleg shrimpOP
const ac = new AbortController();
export async function generateMetadata(
{ params }: { params: {id: string}}
): Promise<Metadata> {
const { id } = params;
const result = await fetch1(ac)(id);
if (!result) {
ac.abort();
notFound();
}
return {
title: result.title,
}
}
export default function Page1({ params }: { params: { id: string } }) {
const { id } = params;
return (
<div className="flex h-screen flex-col">
<div className="grow">
<Suspense fallback={<Loader />}>
<Child1 id={id} ac={ac} />
</Suspense>
<Suspense fallback={<Loader />}>
<Child2 id={id} ac={ac} />
</Suspense>
</div>
</div>
);
}export default async function Child1({ id, ac }: { id: string, ac: AbortController }) {
const result = await fetch1(ac)(id);
if (!result) {
return notFound();
}
return (<div>result.title</div>);
}
export default async function Child2({ id, ac }: { id: string, ac: AbortController }) {
const result = await fetch2(ac)(id);
if (!result) {
return (<div>Failed to fetch</div>);
}
return (<div>result.title</div>);
}export const fetch1 = (abort: AbortController) => async (
id: string,
): Promise<Rec | null> => {
try {
const url = `${process.env.API_URL}/fetch1/${id}`;
const res = await fetch(url, { cache, signal: abort.signal });
// some code
} catch (e) {
return null;
}
};
export const fetch2 = (abort: AbortController) => async (
id: string,
): Promise<Rec | null> => {
try {
// THIS ONE TAKES 10s to load for the purpose of experiment
const url = `${process.env.API_URL}/fetch2/${id}`;
const res = await fetch(url, { cache, signal: abort.signal });
// some code
} catch (e) {
return null;
}
};@Ray btw, I think you could do this instead?
ts
const findPage = cache(async () => {
return null;
});
export async function generateMetadata() {
const page = await findPage();
if (!page) notFound();
}
export default async function Page() {
const page = await findPage();
if (!page) notFound();
return <div></div>;
}
and wrap fetch1 with react cache like this
Whiteleg shrimpOP
export const fetch1C = cache(fetch1);right?
and now iam using fetch1C everywhere
Whiteleg shrimpOP
still same effect, after first invalid id and notFound-abort invocation, all previously opened valid pages become 404, because abortController stays the same on the client side and is already aborted as far as i understand
Whiteleg shrimpOP
oh thank you, but i dont want you to waste your time on reproducing this, i still believe that there is some pattern exist... or iam using nextjs upside down
Answer
Whiteleg shrimpOP
thats the point!
is that what you want?
Whiteleg shrimpOP
yes! tested - this way now it works as I want! thank you! now i got what was wrong in my understanding of how things should be organized!