Connection pool: ensure response is fully read

The streaming API of HTTP::Client has an internal buffer
that will continue to persist onto the next request unless
the response is fully read.

This commit privatizes the #client method of Pool and instead
expose various HTTP request methods that will call and yield
the underlying request and response.

This way, we can ensure that the resposne is fully read before
the client is passed back into the pool for another request.
This commit is contained in:
syeopite 2024-11-14 16:20:23 -08:00
parent 540dfe2927
commit 75eb8b8d7f
No known key found for this signature in database
GPG Key ID: A73C186DA3955A1A
13 changed files with 57 additions and 34 deletions

View File

@ -166,7 +166,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
} }
LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed") LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed")
rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body rss = YT_POOL.get("/feeds/videos.xml?channel_id=#{ucid}").body
LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed") LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed")
rss = XML.parse(rss) rss = XML.parse(rss)

View File

@ -24,8 +24,33 @@ module Invidious::ConnectionPool
@pool = build_pool() @pool = build_pool()
end end
{% for method in %w[get post put patch delete head options] %}
def {{method.id}}(*args, **kwargs)
self.client do | client |
client.{{method.id}}(*args, **kwargs) do | response |
result = yield response
return result
ensure
response.body_io?.try &. skip_to_end
end
end
end
def {{method.id}}(*args, **kwargs)
self.client do | client |
return response = client.{{method.id}}(*args, **kwargs)
ensure
if response
response.body_io?.try &. skip_to_end
end
end
end
{% end %}
# Checks out a client in the pool # Checks out a client in the pool
def client(&) private def client(&)
pool.checkout do |http_client| pool.checkout do |http_client|
# Proxy needs to be reinstated every time we get a client from the pool # Proxy needs to be reinstated every time we get a client from the pool
http_client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy http_client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy

View File

@ -26,7 +26,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
end end
video_id = "CvFH_6DNRCY" if rdid.starts_with? "OLAK5uy_" video_id = "CvFH_6DNRCY" if rdid.starts_with? "OLAK5uy_"
response = YT_POOL.client &.get("/watch?v=#{video_id}&list=#{rdid}&gl=US&hl=en", headers) response = YT_POOL.get("/watch?v=#{video_id}&list=#{rdid}&gl=US&hl=en", headers)
initial_data = extract_initial_data(response.body) initial_data = extract_initial_data(response.body)
if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]? if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]?

View File

@ -21,7 +21,7 @@ module Invidious::Routes::API::Manifest
end end
if dashmpd = video.dash_manifest_url if dashmpd = video.dash_manifest_url
response = YT_POOL.client &.get(URI.parse(dashmpd).request_target) response = YT_POOL.get(URI.parse(dashmpd).request_target)
if response.status_code != 200 if response.status_code != 200
haltf env, status_code: response.status_code haltf env, status_code: response.status_code
@ -163,7 +163,7 @@ module Invidious::Routes::API::Manifest
# /api/manifest/hls_playlist/* # /api/manifest/hls_playlist/*
def self.get_hls_playlist(env) def self.get_hls_playlist(env)
response = YT_POOL.client &.get(env.request.path) response = YT_POOL.get(env.request.path)
if response.status_code != 200 if response.status_code != 200
haltf env, status_code: response.status_code haltf env, status_code: response.status_code
@ -218,7 +218,7 @@ module Invidious::Routes::API::Manifest
# /api/manifest/hls_variant/* # /api/manifest/hls_variant/*
def self.get_hls_variant(env) def self.get_hls_variant(env)
response = YT_POOL.client &.get(env.request.path) response = YT_POOL.get(env.request.path)
if response.status_code != 200 if response.status_code != 200
haltf env, status_code: response.status_code haltf env, status_code: response.status_code

View File

@ -106,7 +106,7 @@ module Invidious::Routes::API::V1::Videos
# Auto-generated captions often have cues that aren't aligned properly with the video, # Auto-generated captions often have cues that aren't aligned properly with the video,
# as well as some other markup that makes it cumbersome, so we try to fix that here # as well as some other markup that makes it cumbersome, so we try to fix that here
if caption.name.includes? "auto-generated" if caption.name.includes? "auto-generated"
caption_xml = YT_POOL.client &.get(url).body caption_xml = YT_POOL.get(url).body
settings_field = { settings_field = {
"Kind" => "captions", "Kind" => "captions",
@ -147,7 +147,7 @@ module Invidious::Routes::API::V1::Videos
query_params = uri.query_params query_params = uri.query_params
query_params["fmt"] = "vtt" query_params["fmt"] = "vtt"
uri.query_params = query_params uri.query_params = query_params
webvtt = YT_POOL.client &.get(uri.request_target).body webvtt = YT_POOL.get(uri.request_target).body
if webvtt.starts_with?("<?xml") if webvtt.starts_with?("<?xml")
webvtt = caption.timedtext_to_vtt(webvtt) webvtt = caption.timedtext_to_vtt(webvtt)
@ -300,7 +300,7 @@ module Invidious::Routes::API::V1::Videos
cache_annotation(id, annotations) cache_annotation(id, annotations)
end end
else # "youtube" else # "youtube"
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}") response = YT_POOL.get("/annotations_invideo?video_id=#{id}")
if response.status_code != 200 if response.status_code != 200
haltf env, response.status_code haltf env, response.status_code

View File

@ -369,7 +369,7 @@ module Invidious::Routes::Channels
value = env.request.resource.split("/")[2] value = env.request.resource.split("/")[2]
body = "" body = ""
{"channel", "user", "c"}.each do |type| {"channel", "user", "c"}.each do |type|
response = YT_POOL.client &.get("/#{type}/#{value}/live?disable_polymer=1") response = YT_POOL.get("/#{type}/#{value}/live?disable_polymer=1")
if response.status_code == 200 if response.status_code == 200
body = response.body body = response.body
end end

View File

@ -92,7 +92,7 @@ module Invidious::Routes::Embed
return env.redirect url return env.redirect url
when "live_stream" when "live_stream"
response = YT_POOL.client &.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}") response = YT_POOL.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}")
video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"] video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"]
env.params.query.delete_all("channel") env.params.query.delete_all("channel")

View File

@ -9,10 +9,10 @@ module Invidious::Routes::ErrorRoutes
item = md["id"] item = md["id"]
# Check if item is branding URL e.g. https://youtube.com/gaming # Check if item is branding URL e.g. https://youtube.com/gaming
response = YT_POOL.client &.get("/#{item}") response = YT_POOL.get("/#{item}")
if response.status_code == 301 if response.status_code == 301
response = YT_POOL.client &.get(URI.parse(response.headers["Location"]).request_target) response = YT_POOL.get(URI.parse(response.headers["Location"]).request_target)
end end
if response.body.empty? if response.body.empty?
@ -40,7 +40,7 @@ module Invidious::Routes::ErrorRoutes
end end
# Check if item is video ID # Check if item is video ID
if item.match(/^[a-zA-Z0-9_-]{11}$/) && YT_POOL.client &.head("/watch?v=#{item}").status_code != 404 if item.match(/^[a-zA-Z0-9_-]{11}$/) && YT_POOL.head("/watch?v=#{item}").status_code != 404
env.response.headers["Location"] = url env.response.headers["Location"] = url
haltf env, status_code: 302 haltf env, status_code: 302
end end

View File

@ -168,7 +168,7 @@ module Invidious::Routes::Feeds
"default" => "http://www.w3.org/2005/Atom", "default" => "http://www.w3.org/2005/Atom",
} }
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}") response = YT_POOL.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
rss = XML.parse(response.body) rss = XML.parse(response.body)
videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry| videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry|
@ -308,7 +308,7 @@ module Invidious::Routes::Feeds
end end
end end
response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}") response = YT_POOL.get("/feeds/videos.xml?playlist_id=#{plid}")
document = XML.parse(response.body) document = XML.parse(response.body)
document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node| document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|

View File

@ -12,7 +12,7 @@ module Invidious::Routes::Images
end end
begin begin
GGPHT_POOL.client &.get(url, headers) do |resp| GGPHT_POOL.get(url, headers) do |resp|
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
rescue ex rescue ex
@ -42,7 +42,7 @@ module Invidious::Routes::Images
end end
begin begin
Invidious::ConnectionPool.get_ytimg_pool(authority).client &.get(url, headers) do |resp| Invidious::ConnectionPool.get_ytimg_pool(authority).get(url, headers) do |resp|
env.response.headers["Connection"] = "close" env.response.headers["Connection"] = "close"
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
@ -65,7 +65,7 @@ module Invidious::Routes::Images
end end
begin begin
Invidious::ConnectionPool.get_ytimg_pool("i9").client &.get(url, headers) do |resp| Invidious::ConnectionPool.get_ytimg_pool("i9").get(url, headers) do |resp|
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
rescue ex rescue ex
@ -81,7 +81,7 @@ module Invidious::Routes::Images
end end
begin begin
YT_POOL.client &.get(env.request.resource, headers) do |response| YT_POOL.get(env.request.resource, headers) do |response|
env.response.status_code = response.status_code env.response.status_code = response.status_code
response.headers.each do |key, value| response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
@ -111,7 +111,7 @@ module Invidious::Routes::Images
if name == "maxres.jpg" if name == "maxres.jpg"
build_thumbnails(id).each do |thumb| build_thumbnails(id).each do |thumb|
thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg" thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg"
if Invidious::ConnectionPool.get_ytimg_pool("i9").client &.head(thumbnail_resource_path, headers).status_code == 200 if Invidious::ConnectionPool.get_ytimg_pool("i9").head(thumbnail_resource_path, headers).status_code == 200
name = thumb[:url] + ".jpg" name = thumb[:url] + ".jpg"
break break
end end
@ -127,7 +127,7 @@ module Invidious::Routes::Images
end end
begin begin
Invidious::ConnectionPool.get_ytimg_pool("i").client &.get(url, headers) do |resp| Invidious::ConnectionPool.get_ytimg_pool("i").get(url, headers) do |resp|
return self.proxy_image(env, resp) return self.proxy_image(env, resp)
end end
rescue ex rescue ex

View File

@ -483,7 +483,7 @@ module Invidious::Routes::Playlists
# Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos # Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos
def self.watch_videos(env) def self.watch_videos(env)
response = YT_POOL.client &.get(env.request.resource) response = YT_POOL.get(env.request.resource)
if url = response.headers["Location"]? if url = response.headers["Location"]?
url = URI.parse(url).request_target url = URI.parse(url).request_target
return env.redirect url return env.redirect url

View File

@ -16,11 +16,11 @@ module Invidious::Search
# Search a youtube channel # Search a youtube channel
# TODO: clean code, and rely more on YoutubeAPI # TODO: clean code, and rely more on YoutubeAPI
def channel(query : Query) : Array(SearchItem) def channel(query : Query) : Array(SearchItem)
response = YT_POOL.client &.get("/channel/#{query.channel}") response = YT_POOL.get("/channel/#{query.channel}")
if response.status_code == 404 if response.status_code == 404
response = YT_POOL.client &.get("/user/#{query.channel}") response = YT_POOL.get("/user/#{query.channel}")
response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404 response = YT_POOL.get("/c/#{query.channel}") if response.status_code == 404
initial_data = extract_initial_data(response.body) initial_data = extract_initial_data(response.body)
ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?) ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?)
raise ChannelSearchException.new(query.channel) if !ucid raise ChannelSearchException.new(query.channel) if !ucid

View File

@ -635,15 +635,13 @@ module YoutubeAPI
LOGGER.trace("YoutubeAPI: POST data: #{data}") LOGGER.trace("YoutubeAPI: POST data: #{data}")
# Send the POST request # Send the POST request
body = YT_POOL.client() do |client| body = YT_POOL.post(url, headers: headers, body: data.to_json) do |response|
client.post(url, headers: headers, body: data.to_json) do |response| if response.status_code != 200
if response.status_code != 200 raise InfoException.new("Error: non 200 status code. Youtube API returned \
raise InfoException.new("Error: non 200 status code. Youtube API returned \ status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \ https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
end
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
end end
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
end end
# Convert result to Hash # Convert result to Hash