YouTube下载应用调用youtubei/v1/search API出现403错误求助
问题
开发YouTube下载应用时,调用YouTube的youtubei/v1/search API获取视频数据,出现403错误:Liste.js:184 POST https://www.youtube.com/youtubei/v1/search?prettyPrint=false net::ERR_ABORTED 403。网站已上线,急需解决该问题。
相关前端代码:
import React, { useContext } from 'react' import { Input, Grid, StatLabel, Stat, Image, StatHelpText, Button, useDisclosure, Spinner, } from '@chakra-ui/react' import { useState, useEffect } from 'react' import styles from './styles.module.css' import DataTypes from './DataTypes' import { useAllContext } from '../../context/AllContextes'; import axios from 'axios' const Liste = () => { const message = { 'tr': { share: "Indirmeyi paylaş", search: "Ara", videos: "tr" }, 'de': { share: "Download teilen", search: "Suchen", videos: "de" }, 'en': { share: "Share Download", search: "Search", videos: "en" }, 'fr':{ share: "Partager Télécharger", search: "Chercher", videos: "fr" }, 'ar':{ share: "مشاركة التنزيل", search: "يبحث", videos: "ar" } } const { lang, setLang } = useAllContext() const [data, setData] = useState(undefined); const [query, setQuery] = useState(""); const [isLoading, setIsLoading] = useState(false) let nothing; const payload = { context: { client: { hl: lang, gl: "TR", remoteHost: "5.176.84.155", deviceMake: "Apple", deviceModel: "", visitorData: "CgtURlU1OFpEcm1uSSiF2YGWBg%3D%3D", userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36,gzip(gfe)", clientName:"WEB", clientVersion: "2.20220701.01.00", osName: "Macintosh", osVersion: "10_15_7", originalUrl: "https://www.youtube.com/results?search_query=halil+ibrahim+g%C3%B6ker", screenPixelDensity: 2, platform: "DESKTOP", clientFormFactor: "UNKNOWN_FORM_FACTOR", configInfo: { appInstallData: "CIXZgZYGELfLrQUQuIuuBRDK7P0SEJje_RIQ1IOuBRDP8v0SEK6ergUQ_6muBRDV8f0SEJH4_BIQ2L6tBQ%3D%3D" }, screenDensityFloat: 2, timeZone: "Europe/Istanbul", browserName: "Chrome", browserVersion: "103.0.0.0", screenWidthPoints: 1047, screenHeightPoints: 824, utcOffsetMinutes: 180, userInterfaceTheme: "USER_INTERFACE_THEME_LIGHT", memoryTotalKbytes: "8000000", mainAppWebInfo: { graftUrl: "/results?search_query=halil+ibrahim+g%C3%B6ker", pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_CAN_BE_INSTALLED", webDisplayMode: "WEB_DISPLAY_MODE_BROWSER", isWebNativeShareAvailable: false } }, user: { lockedSafetyMode: false }, request: { useSsl: true, internalExperimentFlags: [], consistencyTokenJars: [] }, clickTracking: { clickTrackingParams: "CB4Q7VAiEwieyd-rzNr4AhUJyxEIHdXVAzc=" }, adSignalsInfo: { params: [ { key: "dt", value: "1656777861522" }, { key: "flash", value: "0" }, { key: "frm", value: "0" }, { key: "u_tz", value: "180" }, { key: "u_his", value: "4" }, { key: "u_h", value: "1050" }, { key: "u_w", value: "1680" }, { key: "u_ah", value: "940" }, { key: "u_aw", value: "1680" }, { key: "u_cd", value: "30" }, { key: "bc", value: "31" }, { key: "bih", value: "824" }, { key: "biw", value: "1031" }, { key: "brdim", value: "0,25,0,25,1680,25,1695,935,1047,824" }, { key: "vis", value: "1" }, { key: "wgl", value: "true" }, { key: "ca_type", value: "image" } ], bid: "ANyPxKqPl9hE_EFhqYFyXkFTsyGxNzPJ3zmZHYk5oPMRScurkXA0sUGIBXgLlNzSF64Eyyo1E87B" } }, query: "halil ibrahim göker", webSearchboxStatsUrl: "/search?oq=halil ibrahim göker&gs_l=youtube.3..0i71k1l6.0.0.1.498002.0.0.0.0.0.0.0.0..0.0.uqaswc2,ytpso-bo-me=0,ytpsoso-bo-me=0,ytpso-rzp=0,ytpso-bo-bro-mi=24212243,ytpsoso-bo-bro-mi=24212243,cfro=1,ytpso-bo-me=1,ytpsoso-bo-me=1,ytpso-rzp=0,ytpso-bo-bro-mi=24237494,ytpsoso-bo-bro-mi=24237494...0...1ac..64.youtube..0.0.0....0.ndfBkFdQXUg" }; const getSearchList = async (value) => { setIsLoading(true) setData(undefined); payload.query = value; const searchResponse = await fetch('https://www.youtube.com/youtubei/v1/search?prettyPrint=false',{ method: "POST", mode:"no-cors", body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', } }); setIsLoading(false) }; useEffect(() => { if (data) console.log(data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents[0]?.itemSectionRenderer?.contents); }, [data]); return ( <div className={styles.div}> <div className={styles.searchContainer}> <Input height="2.5rem" className={styles.input} variant='unstyled' onChange={(e) => { setQuery(e.target.value); }} placeholder={message[lang].search} value={query} pl="6px"/> <Button ml="6px" mr="5px" onClick={() => getSearchList(query)} bgGradient="linear(to-r, #f3360e, #fb4b18)" color="#fff" variant='solid'>{message[lang].search}</Button> </div> {isLoading ? <Spinner className={styles.spinner} thickness='10px' speed='0.65s' emptyColor='gray.200' color='#f3360e' width="2cm" height="2cm" /> : nothing} <Grid className={styles.grid} templateColumns={{ sm: 'repeat(1, 5fr)', md: 'repeat(2, 5fr)', lg: 'repeat(2, 5fr)', xl: 'repeat(4, 5fr)', "2xl": 'repeat(4, 5fr)' }} gap={0}> {data && data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents[0].itemSectionRenderer?.contents.map((item, index) => { if (item.videoRenderer) return ( <Stat mt="20px" mb="20px" className={styles.stat}> <Image src={(item.videoRenderer.thumbnail.thumbnails[0].url)} className={styles.image} onClick={() => window.open(`https://youtube.com${item.videoRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url}`, '_blank').focus()} /> <StatLabel >{(item.videoRenderer.title.runs[0].text)}</StatLabel> <StatLabel fontFamily="candal" color="grey">{(item.videoRenderer.ownerText.runs[0].text)}</StatLabel> <StatHelpText>{(item.videoRenderer.viewCountText.simpleText)}</StatHelpText> <DataTypes data = {data} index ={index}/> <Button className={styles.share} mt="5px" colorScheme='gray'>{message[lang].share}</Button> </Stat> ) })} </Grid> </div > ) } export default Liste
相关HTML依赖:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
解决方案
错误原因分析
- 非公开API与跨域限制:
youtubei/v1/search是YouTube内部使用的非公开接口,浏览器端直接调用会触发跨域规则,且YouTube会校验请求来源的合法性,拒绝非官方请求。 no-cors模式无效:代码中设置mode:"no-cors"会导致浏览器屏蔽响应内容,同时这种请求模式不符合YouTube的校验逻辑,直接触发403。- 硬编码参数失效:请求payload中的
remoteHost、visitorData等参数为固定值,并非从合法会话中实时获取,YouTube会识别为伪造请求并拦截。
具体解决步骤
搭建后端代理接口
在自有服务器上实现代理,由后端发起对YouTube API的请求,前端调用代理接口,绕过跨域限制。
示例Node.js+Express代理:const express = require('express'); const axios = require('axios'); const app = express(); app.use(express.json()); app.post('/api/youtube-search', async (req, res) => { try { const response = await axios.post('https://www.youtube.com/youtubei/v1/search?prettyPrint=false', req.body, { headers: { 'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); res.json(response.data); } catch (error) { res.status(error.response?.status || 500).send(error.message); } }); app.listen(3001, () => console.log('Proxy server running on port 3001'));前端修改调用逻辑:
const getSearchList = async (value) => { setIsLoading(true); setData(undefined); payload.query = value; try { const searchResponse = await fetch('/api/youtube-search', { method: "POST", body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', } }); const result = await searchResponse.json(); setData(result); } catch (err) { console.error(err); } setIsLoading(false); };切换到YouTube官方公开API
使用YouTube Data API的search.list接口,这是官方支持的合法途径,需先在Google Cloud控制台申请API密钥。
示例调用:const API_KEY = '你的Google API密钥'; const getSearchList = async (value) => { setIsLoading(true); setData(undefined); try { const response = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent(value)}&type=video&key=${API_KEY}`); const result = await response.json(); setData(result); } catch (err) { console.error(err); } setIsLoading(false); };动态提取合法参数(非公开API适配)
若坚持使用youtubei/v1/search,需从当前YouTube页面会话中动态提取visitorData、clientVersion等参数,同时处理请求签名,但该方法不稳定,随时可能被YouTube封禁。
内容的提问来源于stack exchange,提问作者Salih Tekin
相关产品推荐
相关产品推荐

