FileReader: Suppress stale loadend after reentrant read
Blink의 FileReader가 abort, load, error 이벤트 핸들러에서 같은
객체로 새 읽기를 시작했을 때 이전 읽기의 loadend를 잘못 발생시키던 문제를
수정했습니다. terminal event를 dispatch한 뒤 FileReader가 다시LOADING 상태인지 확인하고, 새 읽기가 시작됐다면 이전 operation의loadend를 억제하도록 File API 명세와 동작을 맞췄습니다.
문제 설명
FileReader는 파일이나 Blob을 비동기로 읽지만, 등록된 JavaScript 이벤트
핸들러는 Blink가 이벤트를 dispatch하는 동안 동기적으로 실행됩니다. 따라서
기존 읽기가 완료되거나 중단된 뒤 load, abort, error 이벤트를 처리하는
중에도 같은 FileReader로 새 읽기를 시작할 수 있습니다.
const reader = new FileReader();
reader.onabort = () => {
reader.readAsText(secondBlob);
};
reader.readAsText(firstBlob);
reader.abort();위 코드에서 첫 번째 읽기의 abort 핸들러가 두 번째 읽기를 시작하면reader.readyState는 다시 FileReader.LOADING이 됩니다. 이때 기대되는
terminal event 순서는 다음과 같습니다.
abort(first)
load(second)
loadend(second)그러나 기존 Blink 구현은 abort 이벤트에서 JavaScript로 제어권을 넘겼다가
돌아온 뒤 상태를 다시 확인하지 않고 이전 읽기의 loadend를 무조건
발생시켰습니다.
abort(first)
loadend(first) // 새 읽기가 시작됐으므로 발생하면 안 됨
load(second)
loadend(second)같은 문제가 다음 세 terminal event 경로에 있었습니다.
abort()는abort를 발생시킨 직후loadend를 무조건 발생시켰습니다.DidFinishLoading()은 정상 완료의load직후loadend를 무조건
발생시켰습니다.DidFail()은 읽기 실패의error직후loadend를 무조건
발생시켰습니다.
File API의 read operation 알고리즘은load 또는 error를 발생시킨 뒤 FileReader의 state가 loading이 아닌
경우에만 loadend를 발생시키도록 규정합니다. abort() 알고리즘에도 같은
조건이 있습니다. terminal event 핸들러가 새 읽기를 시작했다면 state가 다시loading이 되므로 이전 읽기의 loadend는 생략해야 합니다.
기존 abort()와 DidFinishLoading()에는 이 조건을 구현해야 한다는crbug.com/1204139 TODO가 있었지만, 실제 코드는 상태와 관계없이loadend를 dispatch하고 있었습니다. DidFail()에도 동일한 상태 검사가
필요했습니다.
해결 내용
1. terminal event 이후 state 재검사
third_party/blink/renderer/core/fileapi/file_reader.cc의 abort(),DidFinishLoading(), DidFail()에 같은 조건을 적용했습니다.
FireEvent(event_type_names::kAbort, task_state);
if (state_ != kLoading) {
FireEvent(event_type_names::kLoadend, task_state);
}각 경로는 먼저 해당 operation의 terminal event인 abort, load, error를
dispatch합니다. 이벤트 핸들러가 실행되는 동안 새 read method가 호출되면state_가 kLoading으로 바뀝니다. C++ 코드로 돌아온 뒤 이 값을 검사해
다음과 같이 동작합니다.
| terminal handler의 동작 | dispatch 후 state_ |
이전 읽기의 loadend |
|---|---|---|
| 새 읽기를 시작하지 않음 | kDone |
발생 |
같은 FileReader로 새 읽기 시작 |
kLoading |
억제 |
별도의 reentrancy flag나 operation id를 추가하지 않고, 명세가 정의한FileReader의 현재 state를 새 operation 시작 여부의 기준으로 사용했습니다.
조건은 세 경로에 동일하게 적용해 정상 완료, 중단, 실패의 동작을 일치시켰고,
기존 TODO도 제거했습니다.
2. abort handler 재진입 WPT 추가
third_party/blink/web_tests/external/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js에
abort handler에서 두 번째 읽기를 시작하는 회귀 테스트를 추가했습니다.
테스트는 첫 번째 1 MiB Blob의 loadstart를 기다린 뒤 abort()를 호출해
실제로 진행 중인 읽기를 중단합니다. abort 핸들러는 같은 reader로"second" Blob을 읽고, 두 번째 읽기의 loadend까지 기다린 뒤 수집된
이벤트 순서를 검증합니다.
assert_array_equals(events, ['abort', 'load', 'loadend']);이 assertion은 첫 번째 읽기의 stale loadend가 끼어들면['abort', 'loadend', 'load', 'loadend']가 되어 실패합니다.
3. load handler 재진입 WPT 추가
같은 WPT 파일에 정상 완료의 load 핸들러에서 두 번째 읽기를 시작하는
테스트도 추가했습니다. 첫 번째 load에서만 새 읽기를 시작하고 두 번째
읽기가 끝날 때까지 기다립니다.
assert_array_equals(events, ['load', 'load', 'loadend']);첫 번째 읽기의 loadend가 억제되고, 두 번의 load 뒤 두 번째 읽기의loadend만 발생해야 통과합니다. .any.js 테스트이므로 Window와 Worker
환경에서 같은 웹 표준 동작을 검증할 수 있습니다.
이 WPT 변경 63줄은 Chromium의 자동 export 과정을 통해
web-platform-tests/wpt PR #62349로
전달됐습니다.
4. error handler 재진입 Blink 테스트 추가
읽기 실패 경로는 실제로 열 수 없는 파일이 필요합니다. 이를 안정적으로
만들기 위해third_party/blink/web_tests/fast/files/file-reader-error-reentrancy.html을
추가했습니다.
테스트는 Blink 테스트 전용 eventSender로 존재하지 않는 파일을<input type="file">에 전달하고 읽기를 시작합니다. error 핸들러에서는
다음을 확인한 뒤 같은 reader로 정상적인 두 번째 Blob을 읽습니다.
- 첫 번째 읽기의
readyState가DONE인지 확인합니다. - 오류가
NotFoundError인지 확인합니다. - 두 번째 읽기를 시작한 직후
readyState가LOADING인지 확인합니다. - 최종 result가
"second"인지 확인합니다. - 이벤트 순서가
['error', 'load', 'loadend']인지 확인합니다.
이 테스트는 브라우저 외부의 실제 파일 오류를 제어해야 하므로 portable WPT가
아닌 Blink 내부 web test로 작성했습니다. 이를 통해 WPT의 abort/load 경로와
함께 세 terminal event 경로를 모두 회귀 테스트로 보호합니다.
5. 변경 파일과 규모
최종 Chromium 커밋은 다음 3개 파일을 변경했습니다.
third_party/blink/renderer/core/fileapi/file_reader.cc- 9줄 추가, 7줄 삭제
abort(),DidFinishLoading(),DidFail()의 조건부loadend처리
third_party/blink/web_tests/external/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js- 63줄 추가
- abort와 load handler의 reentrant read 회귀 테스트
third_party/blink/web_tests/fast/files/file-reader-error-reentrancy.html- 54줄 추가
- error handler의 reentrant read 회귀 테스트
전체 변경 규모는 126줄 추가, 7줄 삭제입니다. 최종 커밋은aa471e55eb0098b8141f2a0973f59a4c0368ad68이며 Chromium main의refs/heads/main@{#1690598}에 병합됐습니다.
테스트 방법
1. abort 재진입 WPT
첫 번째 읽기를 중단한 abort 핸들러에서 두 번째 읽기를 시작하고 다음을
검증합니다.
- 첫 번째 읽기의
abort는 한 번 발생합니다. - 첫 번째 읽기의
loadend는 발생하지 않습니다. - 두 번째 읽기는 정상적으로
load와loadend를 발생시킵니다. - 전체 terminal event 순서는
abort → load → loadend입니다.
2. load 재진입 WPT
첫 번째 읽기의 load 핸들러에서 두 번째 읽기를 시작하고 다음을 검증합니다.
- 두 읽기의
load는 각각 한 번씩 발생합니다. - 첫 번째 읽기의
loadend는 발생하지 않습니다. - 두 번째 읽기의
loadend만 마지막에 발생합니다. - 전체 terminal event 순서는
load → load → loadend입니다.
3. error 재진입 Blink web test
존재하지 않는 파일에서 NotFoundError를 발생시킨 뒤 error handler에서
두 번째 Blob 읽기를 시작하고 다음을 검증합니다.
- 첫 번째 읽기의 오류 상태와
NotFoundError가 올바릅니다. - error handler 안에서 두 번째 읽기가
LOADING상태로 전환됩니다. - 두 번째 읽기의 결과가
"second"입니다. - 전체 terminal event 순서는
error → load → loadend입니다.
4. Gerrit CQ와 WPT export
Patch Set 2는 Chromium LUCI CQ dry run을 통과했습니다. Rakina Zata Amni와
Fergal Daly의 코드 리뷰를 거쳐 full CQ가 변경을 검증했으며, 최종 제출
과정에서 Patch Set 3으로 rebase된 뒤 2026년 9월 2일 main에 병합됐습니다.
Blink W3C Test Autoroller는 export 가능한 WPT 변경을 감지해 WPT PR
#62349를 생성했습니다. Chromium CL이 병합된 뒤 PR의 do not merge yet
label이 제거되어 upstream 반영 절차로 이어졌습니다.
별도의 성능 테스트는 수행하지 않았습니다. 이 변경은 파일 읽기나 데이터
처리 경로를 바꾸지 않고 terminal event 뒤의 상태 검사와 event dispatch
여부만 수정합니다.
배운 점
- 비동기 API에서도 event dispatch 자체는 동기적이므로 JavaScript callback이
실행되는 동안 C++ 객체의 상태가 바뀌는 reentrancy를 고려해야 합니다. - callback을 호출하기 전에 확인한 상태는 callback 이후에도 유효하다고
가정할 수 없습니다. 이번 수정처럼 외부 코드가 실행된 직후 상태를 다시
검사해야 합니다. loadend는 각 읽기 작업의 마지막 event라는 의미를 가집니다. 새 작업이
이미 시작된 상황에서 이전 작업의loadend를 발생시키면 event listener가
현재 작업의 종료로 잘못 해석할 수 있습니다.- 명세의 "state가 loading이 아닌 경우"라는 짧은 조건 하나가 abort, 정상
완료, 실패의 세 구현 경로에 모두 대응합니다. 표준 알고리즘을 실제 코드의
모든 terminal path와 대조해야 일부 경로만 수정하는 실수를 피할 수
있습니다. - 정상 완료와 abort는 Blob만으로 portable WPT를 작성할 수 있지만, 파일 읽기
실패는 재현 가능한 OS 파일 오류가 필요합니다. 웹에서 관찰 가능한 동작은
WPT로 공유하고 Chromium 전용 test harness가 필요한 부분은 Blink web test로
보완하는 계층화가 유용했습니다. - 이벤트 개수를 따로 확인하는 것보다 전체 순서를 배열로 기록해 비교하면
stale event의 삽입 위치까지 명확하게 검증할 수 있습니다. - Chromium의
external/wpt변경은 Blink W3C Test Autoroller가 upstream
WPT PR로 export합니다. 구현 수정과 함께 표준 테스트를 추가하면 다른
브라우저도 같은 회귀 케이스를 공유할 수 있습니다.