# Nhúng YouTube vào iframe: hành trình qua renderer crash, lỗi 152 và lỗi 153

**URL:** <https://diembao.net/t/nhung-youtube-vao-iframe-hanh-trinh-qua-renderer-crash-loi-152-va-loi-153/1607>\
**Category:** Công Nghệ\
**Created:** [August 24, 2026, 2:34pm UTC](https://diembao.net/t/nhung-youtube-vao-iframe-hanh-trinh-qua-renderer-crash-loi-152-va-loi-153/1607 "2026-08-24T14:34:08Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![tran\_tien](https://diembao.net/user_avatar/diembao.net/tran_tien/32/2850_2.png) [@tran\_tien](https://diembao.net/u/tran_tien)\
**Post date:** [August 24, 2026, 2:34pm UTC](https://diembao.net/t/nhung-youtube-vao-iframe-hanh-trinh-qua-renderer-crash-loi-152-va-loi-153/1607/1 "2026-08-24T14:34:09Z")

</div>

# Nhúng YouTube vào iframe: hành trình qua renderer crash, lỗi 152 và lỗi 153

 ![Youtube sad face](https://diembao.net/uploads/default/original/2X/5/51ad70a990bcc7bfb7a98810784cc931cf45167d.png)

> Ghi chép kỹ thuật từ quá trình debug thực tế trên một Chrome extension (Manifest V3). Nếu bạn từng gỡ header `X-Frame-Options` để nhúng YouTube vào iframe và thấy nó “gần chạy được”, bài này dành cho bạn.

## Bối cảnh

Mình có một extension thay thế trang New Tab (kiểu start page), trong đó có tính năng **nhúng website bất kỳ vào iframe** ngay trên trang chủ. Để vượt qua các cơ chế chống nhúng, extension dùng `declarativeNetRequest` (DNR) gỡ các response header của mọi request `sub_frame`:

```json
{
  "id": 1001,
  "priority": 1,
  "action": {
    "type": "modifyHeaders",
    "responseHeaders": [
      { "header": "X-Frame-Options", "operation": "remove" },
      { "header": "Content-Security-Policy", "operation": "remove" },
      { "header": "Cross-Origin-Opener-Policy", "operation": "remove" },
      { "header": "Cross-Origin-Embedder-Policy", "operation": "remove" },
      { "header": "Cross-Origin-Resource-Policy", "operation": "remove" }
    ]
  },
  "condition": {
    "urlFilter": "*",
    "resourceTypes": ["sub_frame"]
  }
}

```

Với YouTube, cách này hoạt động… một nửa:

- ✅ Trang chủ `youtube.com` load bình thường trong iframe, cuộn xem thoải mái.
- ❌ **Click vào video bất kỳ → toàn bộ iframe chết** , hiện một ô xám với icon “mặt buồn”.

## Bước 1: Chẩn đoán — bị chặn hay bị crash?

Phản xạ đầu tiên khi thấy iframe xám là nghĩ đến `X-Frame-Options`. Nhưng có 3 bằng chứng cho thấy đây **không phải bị chặn header** :

1. **Không có message** `Refused to display ... in a frame` trong console của trang cha — thông báo đặc trưng khi Chrome chặn frame vì XFO/CSP.
2. **Không có request tài liệu mới nào** được tạo ra khi click video. YouTube là SPA: click thumbnail không load lại trang mà chuyển route bằng JavaScript. Không có request thì không có header nào để mà chặn.
3. Icon trong ô xám là **mặt buồn mắt X** — đây là giao diện “Aw, Snap!” phiên bản sub-frame của Chrome, tức là **renderer process của frame đã chết**.

Kết luận: trang watch đầy đủ của YouTube (`/watch?v=...`) **làm crash renderer** khi chạy bên trong iframe. Đây là hành vi nằm ngoài tầm kiểm soát của trang nhúng — không sửa được bằng cách chỉnh thuộc tính iframe (mình đã thử có/không `sandbox`, kết quả như nhau).

**Mẹo xác nhận nhanh:** mở Task Manager của Chrome (`Shift+Esc`), tìm dòng `Subframe: https://www.youtube.com`. Click video trong iframe → dòng này biến mất đúng lúc frame hiện mặt buồn.

**Mẹo tái hiện độc lập:** vì rule DNR áp dụng cho _mọi_ `sub_frame` trên _mọi_ trang, bạn có thể dựng môi trường test trên bất kỳ trang nào (mình dùng `example.com` + DevTools console) thay vì phải test trong extension — tiện hơn rất nhiều khi cần lặp lại nhiều lần.

## Bước 2: Đổi chiến lược — dùng embed player chính thức

Trang watch không sống được trong iframe, nhưng **player nhúng chính thức** `https://www.youtube.com/embed/VIDEO_ID` thì được thiết kế đúng cho việc này, và thực tế chạy hoàn hảo trong cùng điều kiện.

Vậy kiến trúc mới: **chặn click vào video, chuyển hướng iframe sang embed player** thay vì để SPA của YouTube chuyển sang trang watch (và crash).

Hai lớp:

**Lớp 1 — content script** chạy trong frame YouTube (khai báo `all_frames: true`, `run_at: document_start`), chỉ kích hoạt khi bị nhúng:

```js
(function () {
  if (window === window.top) return; // tab YouTube bình thường: không đụng vào

  document.addEventListener('click', (event) => {
    if (event.button !== 0) return;
    // composedPath để bắt được link nằm trong shadow DOM
    const path = event.composedPath ? event.composedPath() : [];
    let link = path.find(el => el instanceof HTMLAnchorElement && el.href);
    if (!link && event.target?.closest) link = event.target.closest('a[href]');
    if (!link || link.target === '_blank') return;

    const videoId = videoIdFromUrl(link.href); // parse /watch?v=, /shorts/, youtu.be
    if (!videoId) return;

    event.preventDefault();
    event.stopImmediatePropagation();
    openEmbed(toEmbedUrl(link.href, videoId));
  }, true); // capture phase!
})();

```

Hai chi tiết quan trọng:

- **Capture phase + `document_start`** : listener của mình phải chạy _trước_ router SPA của YouTube. Đăng ký ở capture phase từ `document_start` đảm bảo điều đó.
- **`composedPath()`**: YouTube dùng custom elements; `event.target` có thể bị retarget khi link nằm trong shadow DOM.

**Lớp 2 — DNR redirect** cho trường hợp URL watch được load thẳng làm src của iframe:

```json
{
  "id": 1002,
  "priority": 2,
  "action": {
    "type": "redirect",
    "redirect": { "regexSubstitution": "https://www.youtube.com/embed/\\1" }
  },
  "condition": {
    "regexFilter": "^https?://(?:www\\.|m\\.)?youtube\\.com/watch\\?(?:.*&)?v=([A-Za-z0-9_-]{11}).*",
    "resourceTypes": ["sub_frame"]
  }
}

```

Reload extension, click video và… hết crash! Nhưng thay vào đó là màn hình đen với dòng chữ:

> **Video này không hoạt động. Mã lỗi: 152-4**

## Bước 3: Mê cung referrer — lỗi 152 và 153

Đây là phần thú vị nhất. Qua một loạt thử nghiệm A/B, mình vẽ ra được “ma trận referrer” của embed player YouTube (thời điểm 2025–2026, sau khi YouTube siết chính sách nhúng):

| `document.referrer` của trang embed | Kết quả |
| --- | --- |
| Một origin web bên ngoài (vd `https://example.com/`) | ✅ Phát bình thường |
| `https://www.youtube.com/` (tự nhúng) | ❌ **Lỗi 152** |
| Rỗng (không có referrer) | ❌ **Lỗi 153** — “Lỗi cấu hình trình phát video” |

Ba phát hiện quan trọng, mỗi cái tốn kha khá thời gian:

### 1. Player đọc `document.referrer`, không đọc HTTP header

Phản xạ của dân extension là “thiếu Referer thì dùng DNR đặt Referer”:

```json
{
  "action": {
    "type": "modifyHeaders",
    "requestHeaders": [
      { "header": "Referer", "operation": "set", "value": "https://example.com/" }
    ]
  }
}

```

**Vô dụng.** Kiểm chứng: iframe với `referrerpolicy="no-referrer"` (mô phỏng không có referrer) trong khi DNR vẫn đặt header → vẫn lỗi 153. Lý do: `document.referrer` là giá trị **trình duyệt tự tính từ ngữ cảnh điều hướng** , JavaScript của player đọc giá trị này ở client — nó không liên quan gì đến header `Referer` mà DNR đã sửa trên đường mạng.

### 2. Các tham số chính thức không thay thế được referrer

`?origin=...`, `?widget_referrer=...`, `?enablejsapi=1` — tất cả đều không qua được check. `youtube-nocookie.com` cũng áp cùng chính sách (chỉ khác giao diện báo lỗi).

### 3. Trang `chrome-extension://` không bao giờ gửi referrer

Đây là nút thắt với extension: khi trang cha là trang của extension, **mọi điều hướng nó khởi xướng đều không mang referrer** (Chrome không gửi referrer từ scheme `chrome-extension://`). Tức là:

- Điều hướng từ _bên trong_ frame YouTube → referrer là [youtube.com](http://youtube.com) → lỗi 152.
- Điều hướng do _trang extension_ khởi xướng → không có referrer → lỗi 153.

Bế tắc cả hai đường.

## Bước 4 (ngõ cụt đáng nhớ): tự bắn vào chân bằng fallback

Trước khi tìm ra giải pháp cuối, mình còn dính một bug tự gây rất “đời”: content script gửi `postMessage` nhờ trang cha đổi src iframe, kèm một fallback tự điều hướng sau 300ms phòng khi trang cha không phản hồi:

```js
window.parent.postMessage({ type: 'open-embed', url }, '*');
setTimeout(() => { location.href = url; }, 300); // ← thủ phạm

```

Vấn đề: trang cha nhận message và đổi src **ngay lập tức** , nhưng điều hướng mạng cần _hơn 300ms_ để commit. Trong lúc đó, document cũ của YouTube **vẫn còn sống** — và timer của nó nổ, tự điều hướng trong frame, **ghi đè** điều hướng sạch của trang cha bằng một điều hướng mang referrer [youtube.com](http://youtube.com). Kết quả: lỗi 152 quay lại, dù cơ chế postMessage hoạt động hoàn hảo.

Bài học: **một điều hướng “đang bay” có thể bị điều hướng khác phát ra từ document cũ ghi đè.** Fallback dạng timeout cần cơ chế hủy. Giải pháp: trang cha gửi ACK ngược lại, content script nhận ACK thì `clearTimeout`:

```js
// Trang cha (trước khi đổi src):
event.source.postMessage({ type: 'embed-ack' }, event.origin);

// Content script:
window.addEventListener('message', (e) => {
  if (e.data?.type === 'embed-ack') clearTimeout(fallbackTimer);
});

```

## Bước 5: Giải pháp cuối — “referrer bounce”

Quay lại bài toán gốc: cần cho embed một `document.referrer` là **origin https thật, không phải YouTube** , trong khi trang cha là `chrome-extension://`. Không thể giả mạo referrer — nhưng có thể **tạo ra một điều hướng thật** từ một trang https thật.

Ý tưởng: dùng một **trang trung gian (bounce)**. Extension khai báo thêm một content script trên `example.com`:

```json
{
  "matches": ["*://example.com/*"],
  "js": ["scripts/yt_bounce.js"],
  "all_frames": true,
  "run_at": "document_start"
}

```

Luồng hoàn chỉnh khi người dùng click video trong iframe:

```auto
Click video trong frame YouTube
  │ (content script chặn ở capture phase — SPA không kịp chạy, không crash)
  ▼
postMessage lên trang cha ──► trang cha gửi ACK (hủy fallback)
  │
  ▼
Trang cha đổi iframe.src = https://example.com/#hl-embed=<url-embed đã encode>
  │ (fragment KHÔNG được gửi lên server example.com — không rò rỉ video ID)
  ▼
yt_bounce.js chạy trên example.com, xác thực URL trong fragment
  │
  ▼
location.replace(embedUrl)
  │ (điều hướng khởi xướng từ document example.com
  │ → document.referrer = "https://example.com/" — HỢP LỆ)
  ▼
Embed player phát video ✅

```

Nội dung `yt_bounce.js` gọn nhẹ:

```js
(function () {
  if (window === window.top) return;

  const match = location.hash.match(/^#hl-embed=(.+)$/);
  if (!match) return;

  let target = '';
  try { target = decodeURIComponent(match[1]); } catch { return; }

  // Chỉ chấp nhận đúng URL embed của YouTube — không làm open redirect
  if (!/^https:\/\/www\.youtube(-nocookie)?\.com\/embed\/[A-Za-z0-9_-]{11}([/?#].*)?$/.test(target)) return;

  location.replace(target);
})();

```

Vài lựa chọn thiết kế đáng nói:

- **Fragment thay vì query string** : `#hl-embed=...` không bao giờ được gửi trong HTTP request, nên server của trang bounce chỉ thấy một request trang trống — video người dùng xem không bị lộ.
- **Xác thực URL bằng regex chặt** ở cả hai đầu (trang cha lẫn bounce script) để bounce không thể bị lợi dụng thành open redirect.
- **Kiểm tra `window === window.top`** để content script không làm gì khi người dùng thật sự ghé thăm trang bounce.
- Trang bounce cần là một trang https **nhẹ, ổn định, ít người ghé** — và content script thoát ngay lập tức nếu không có fragment đúng định dạng.

Bonus: content script trên YouTube còn xử lý luôn trường hợp trang embed bị load thẳng từ trang extension (cấu hình sẵn trong settings) — phát hiện `document.referrer === ''` trên trang `/embed/` và nhờ trang cha nạp lại qua bounce:

```js
if (/^\/embed\//.test(location.pathname) && document.referrer === '') {
  window.parent.postMessage({ type: 'open-embed', url: location.href }, '*');
  return;
}

```

## Kết quả và giới hạn

Sau 5 vòng debug: click video bất kỳ trên trang chủ YouTube trong iframe → video phát ngay trong embed player, không crash, không 152, không 153.

Giới hạn còn lại (nằm ở phía YouTube, không vượt được):

- Video mà chủ kênh **tắt cho phép nhúng** sẽ hiện “Chủ sở hữu video đã tắt tính năng phát trên các trang web khác” — đúng như trên mọi website nhúng khác. Người dùng bấm “Xem trên YouTube” để mở tab mới.
- Embed player chỉ có video thuần — không có mô tả, bình luận, sidebar đề xuất.
- Autoplay đôi khi không tự kích hoạt vì user activation bị tiêu hao qua hai lần điều hướng — người dùng bấm play thêm một lần.

## Tóm tắt bài học

1. **Phân biệt “bị chặn” và “bị crash”** : ô xám mặt buồn + không có log `Refused to display` + không có request mạng = renderer crash, không phải XFO. Task Manager của Chrome (`Shift+Esc`) là công cụ xác nhận nhanh nhất.
2. **Trang watch đầy đủ của YouTube không sống được trong iframe** — đừng cố; embed player là đường được hỗ trợ.
3. **YouTube embed (2025+) yêu cầu `document.referrer` là một origin https bên ngoài**: rỗng → 153, chính [youtube.com](http://youtube.com) → 152.
4. **DNR sửa được HTTP header nhưng không sửa được `document.referrer`** — mọi check thực hiện bằng JavaScript phía client đều miễn nhiễm với `modifyHeaders`.
5. **Trang `chrome-extension://` không gửi referrer** — muốn có referrer thật, phải để một trang https thật khởi xướng điều hướng (kỹ thuật bounce).
6. **Fallback timeout phải hủy được** : điều hướng đang bay có thể bị document cũ ghi đè; dùng ACK qua `postMessage`.
7. **Tái hiện lỗi bên ngoài môi trường gốc** (rule DNR áp dụng toàn cục nên test được trên trang thường) giúp vòng lặp debug nhanh hơn nhiều lần.

* * *

_Bài viết dựa trên quá trình debug thực tế trên Chrome (Windows), tháng 8/2026. Hành vi của YouTube có thể thay đổi theo thời gian — các mã lỗi 152/153 và chính sách referrer được ghi nhận tại thời điểm viết._
