Repository navigation
Conversation
Refs #683 Also carries the identical changepack fix from #788 so the changepack guard passes on this base. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Takes main's dependency ranges from #768 (vinext 1.2.1, @vitejs/plugin-rsc 0.5.38) and regenerates bun.lock with bun install. Refs #683 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
Contributor
Author
|
메인테이너 결정으로 닫습니다. 저장소에서 의존성(vinext)을 패치하지 않습니다. 이 PR에서 확인한 원인과 재현 근거는 upstream(cloudflare/vinext)에 이슈와 수정 PR로 제출합니다. upstream에 수정이 반영되어 배포되면, 그 버전으로 vinext를 올리는 것으로 해결합니다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #683 (landing 빌드 신뢰성: Vinext 사전 렌더링이 페이지를 조용히 건너뛰는 문제). main 기반 단독 PR이며 먼저 병합되어야 할 PR은 없습니다(#788의 changepack 수정과 #768의 의존성 범위는 이미 main에 있습니다).
요약
landing 앱을
vinext build로 빌드할 때 사전 렌더링 단계가 가끔 페이지를 건너뛰고도 종료 코드 0으로 끝났습니다. 원인은 Vinext의 빌드용 HTTP 클라이언트가, Vinext 자신이 띄운 로컬 사전 렌더링 서버가 keep-alive 시간 초과로 이미 닫고 있는 유휴 연결을 다시 쓰는 것입니다. 요청은ECONNRESET으로 실패하고, Vinext는 그 페이지를 오류가 아니라 "dynamic이라 건너뜀"으로 기록합니다.이 PR은 다음을 합니다.
patches/vinext@1.2.1.patch, 루트patchedDependencies)로 Vinext 1.2.1의 빌드용 사전 렌더링 서버만 연결당 요청 1개(maxRequestsPerSocket = 1)로 바꿉니다. 유휴 연결이 재사용되지 않으므로 경쟁 조건이 사라집니다.apps/landing/check-prerender.ts)를 실행해, 기대한 70개 경로의 HTML과 Flight(.txt), 그리고404.html중 하나라도 없거나 비어 있으면 빌드가 실패합니다.원인
배포된 Vinext 1.2.1 코드, Node 24.19.0(내장 undici 7.29.0)에서 확인했습니다.
prerenderApp은 같은 프로세스에서 띄운 Vinext 서버(startProdServer({ purpose: "prerender" }))에 전역fetch()로 요청합니다. undici는 keep-alive 연결을 풀에 두고 다음 요청에 재사용합니다.keepAliveTimeout(5000ms) +keepAliveTimeoutBuffer(1000ms)보다 오래 막으면, 루프가 풀린 직후 같은 타이머 단계에서 undici의 유휴 소켓 검증 타이머가 먼저 실행되어 요청을 그 소켓에 쓰고, 이어서 서버의socketOnTimeout이 같은 소켓을 파괴합니다. 서버는 요청을 읽기 전이므로 처리하지 않고, 클라이언트는read ECONNRESET을 받습니다.prerender.js의renderUrl은 분류가unknown인(추측 렌더링) 경로에서 던져진 오류를 모두skipped/dynamic으로 바꿉니다. 404 센티널 요청 실패도 삼킵니다. export 검사(run-prerender.js)는error상태만 보므로 빌드는 종료 코드 0으로 끝나고 HTML과 Flight 파일이 빠집니다.재현 근거(모두 실제 배포 코드, 6 GiB 상한):
socketOnTimeout파괴 →read ECONNRESET, 서버 핸들러 미도달/victimskipped/dynamic, HTML·Flight 없음Connection: close)maxRequestsPerSocket = 1)Connection: close, 풀에 유휴 연결 없음, 새 튜플같은 조건에서 패치 없이 돌린 landing 실제 빌드 1회(W71b, 패치 전)는 70개 경로 모두 정상이었습니다. 이 경쟁은 이벤트 루프가 keep-alive 시간보다 오래 막힐 때만 생기므로 평소 빌드에서는 드물게 나타나고, 그래서 가끔 페이지가 빠지는 증상이 됩니다.
변경
Vinext 패치 (
patches/vinext@1.2.1.patch)dist/server/prod-server.jsapplyPrerenderConnectionPolicy(server, purpose):purpose === "prerender"일 때만server.maxRequestsPerSocket = 1. App Router와 Pages Router 서버 생성 직후에 적용합니다. Vinext가 빌드 때 띄우는 모든 서버(App, Pages, App+Pages 공유, 병렬 렌더 워커, 경로 탐색)가 이purpose를 씁니다.vinext start, standalone 같은 사용자 서버는purpose가 없으므로 Node 기본값 그대로입니다. 클라이언트 쪽fetch, 연결 풀, 시간 초과, 동시성은 바꾸지 않았고 재시도나 대기도 넣지 않았습니다.dist/build/prerender.jsfetchPrerenderServer(): 사전 렌더링 서버로 가는fetch()가 응답 없이 거부되면 그 오류 객체를 표시합니다(WeakSet, 오류 객체는 바꾸지 않음).createExportRequestFailures(mode): export 모드에서만, 표시된 오류를 모든 catch 지점(App 렌더, App 404,generateStaticParams, Pages 렌더, Pages 404,getStaticPaths)에서 경로와 함께 기록하고, 진행 중인 렌더가 모두 끝난 뒤(병렬 워커 상태 검사와 manifest 쓰기 전) 한 번에 실패시킵니다.no-store, 비정상 상태 코드, 스트림 도중 중단)는 지금처럼 분류합니다. 즉 실제 dynamic 경로는 그대로skipped/dynamic입니다. export가 아닌 모드도 그대로입니다.landing 산출물 검사
apps/landing/verify-prerender.ts:e2e/exported-routes.ts의 70개 경로마다 Vinext의getOutputPath/getRscOutputPath로 HTML과 Flight 경로를 계산해 일반 파일이고 비어 있지 않은지 확인하고,404.html도 확인합니다.apps/landing/check-prerender.ts를build스크립트 끝(vinext build && bun ./check-prerender.ts)에서 실행합니다.새로 생기는 오류
AggregateError,cause는 첫 원래 오류,errors는 모든 원래 오류, 경로는 정렬):동적 경로는
/blog/:slug (/blog/a)처럼 패턴과 실제 경로를 함께 씁니다. 404는/404로 표시합니다.PrerenderArtifactError,route/path/reason필드):reason은missing,empty,not-file중 하나입니다.버전 업데이트
이 브랜치는 처음에 세 워크스페이스(
apps/landing,apps/vinext-rsc-css-repro,benchmark/vinext-devup-ui)의 Vinext를 최신 1.2.1로,@vitejs/plugin-rsc를 0.5.38로, React 계열 peer를 19.3.1로 올렸습니다. 같은 업데이트가 #768로 main에 먼저 들어갔으므로, main 병합(689e34e) 때 이 범위들은 main의 줄을 그대로 따랐습니다. 그래서 이 PR이 바꾸는 의존성 범위는 없고, 루트patchedDependencies가 main이 쓰는 정확한 버전vinext@1.2.1에 패치를 연결합니다.bun.lock은bun install로 다시 만들었고 main 대비 차이는patchedDependencies세 줄뿐입니다. 패치 전 1.2.1 업데이트 상태에서apps/vinext-rsc-css-repro의 RSC CSS 단언은 바꾸지 않았습니다. 배포되는 패키지 내용은 바뀌지 않으므로 changepack은 없습니다.검증
회귀 테스트
packages/vite-plugin/src/__tests__/vinext-prerender-transport.test.ts는 landing 앱이 쓰는 Vinext 파일을 찾아 Node로vinext-prerender-transport.helper.mjs를 실행합니다(vinext build가 Node에서 돌기 때문입니다). 헬퍼는 실제 배포 코드인prerenderApp을 Vinext 자신의startProdServer가 띄운 서버에 대고 실행합니다.shipped: Vinext가 사전 렌더링용으로 띄운 서버 그대로. keep-alive warm 요청 뒤 이벤트 루프를keepAliveTimeout + keepAliveTimeoutBuffer(6000ms)만큼 막습니다. 기대: warm 응답connection: close,maxRequestsPerSocket1,/victimrendered와 정확한 HTML·Flight,no-store인/dynamic은 그대로 skipped/dynamic.reuse: 같은 서버에서maxRequestsPerSocket을 Node 기본값 0으로 되돌린 대조군. 기대: 페이지가 조용히 빠지지 않음(렌더되거나/victim을 밝힌 export 오류).reset: 요청을 받으면 TCP 리셋하는node:net서버. 기대: 위 "새로 생기는 오류" 1번 메시지 그대로,cause체인TypeError: fetch failed←ECONNRESET, 파일 없음.purpose없는startProdServer는maxRequestsPerSocket0 그대로.Node 24.19에서 패치를 적용한 헬퍼의 실제 출력(요약):
그 밖의 검증:
Prerendered 71 routes (0 skipped),check-prerender.ts통과, HTML 71개(70개 경로 +404.html)와 Flight 70개.landing-prerender.test.ts17개 통과(누락, 빈 파일, 디렉터리, 파일 시스템 오류 전파).bun test6363 pass / 0 fail, main 병합 689e34ebun test6596 pass / 0 fail, 둘 다 함수·라인 100%.bun lint통과. 로컬 Windows tarpaulin은 97.87%이고 Linux CI가 100%를 강제합니다.bun,node타입) 통과.남는 한계
patchedDependencies의 키가 정확한 버전). Vinext를 올리면 Bun이 패치 적용 실패로 설치를 멈추므로, 그때 업스트림 수정 여부를 보고 패치를 옮기거나 지웁니다. 업스트림 이슈와 수정 PR은 따로 준비합니다.latest)에서는 같은 재현 순서로 페이지가 빠지지 않았습니다. 그래서 회귀 테스트는 모든 Node에서 서버 정책과 결정적인 연결 리셋 경로를 확인하고, 실제 경쟁은 재현되는 Node(24)에서만 확인합니다. Node 26에서 다른 타이밍으로 경쟁이 남아 있는지는 증명하지 않았습니다.