Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/braintrust/api/datasets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "json"
require "uri"
require_relative "../logger"
require_relative "../internal/http"

module Braintrust
class API
Expand Down Expand Up @@ -111,6 +112,7 @@ def fetch(id:, limit: 1000, cursor: nil, version: nil)
payload[:version] = version if version

response = http_post_json_raw("/btql", payload)
Braintrust::Internal::Http.decompress_response!(response)

# Parse JSONL response
records = response.body.lines
Expand Down Expand Up @@ -158,9 +160,7 @@ def http_request(method, path, params: {}, payload: nil, base_url: nil, parse_js
start_time = Time.now
Log.debug("[API] #{method.upcase} #{uri}")

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
response = http.request(request)
response = Braintrust::Internal::Http.with_redirects(uri, request)

duration_ms = ((Time.now - start_time) * 1000).round(2)
Log.debug("[API] #{method.upcase} #{uri} -> #{response.code} (#{duration_ms}ms, #{response.body.bytesize} bytes)")
Expand Down
5 changes: 2 additions & 3 deletions lib/braintrust/api/functions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "json"
require "uri"
require_relative "../logger"
require_relative "../internal/http"

module Braintrust
class API
Expand Down Expand Up @@ -242,9 +243,7 @@ def http_request(method, path, params: {}, payload: nil, parse_json: true)
start_time = Time.now
Log.debug("[API] #{method.upcase} #{uri}")

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
response = http.request(request)
response = Braintrust::Internal::Http.with_redirects(uri, request)

duration_ms = ((Time.now - start_time) * 1000).round(2)
Log.debug("[API] #{method.upcase} #{uri} -> #{response.code} (#{duration_ms}ms, #{response.body.bytesize} bytes)")
Expand Down
8 changes: 2 additions & 6 deletions lib/braintrust/api/internal/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "json"
require "uri"
require_relative "../../logger"
require_relative "../../internal/http"

module Braintrust
class API
Expand Down Expand Up @@ -44,12 +45,7 @@ def self.login(api_key:, app_url:, org_name: nil)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{api_key}"

http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = true if uri.scheme == "https"

response = http.start do |http_session|
http_session.request(request)
end
response = Braintrust::Internal::Http.with_redirects(uri, request)

Log.debug("Login: received response [#{response.code}]")

Expand Down
5 changes: 2 additions & 3 deletions lib/braintrust/api/internal/experiments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "net/http"
require "json"
require "uri"
require_relative "../../internal/http"

module Braintrust
class API
Expand Down Expand Up @@ -41,9 +42,7 @@ def create(name:, project_id:, ensure_new: true, tags: nil, metadata: nil,
request["Authorization"] = "Bearer #{@state.api_key}"
request.body = JSON.dump(payload)

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
response = http.request(request)
response = Braintrust::Internal::Http.with_redirects(uri, request)

unless response.is_a?(Net::HTTPSuccess)
raise Error, "HTTP #{response.code} for POST #{uri}: #{response.body}"
Expand Down
5 changes: 2 additions & 3 deletions lib/braintrust/api/internal/projects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "net/http"
require "json"
require "uri"
require_relative "../../internal/http"

module Braintrust
class API
Expand All @@ -26,9 +27,7 @@ def create(name:)
request["Authorization"] = "Bearer #{@state.api_key}"
request.body = JSON.dump({name: name})

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
response = http.request(request)
response = Braintrust::Internal::Http.with_redirects(uri, request)

unless response.is_a?(Net::HTTPSuccess)
raise Error, "HTTP #{response.code} for POST #{uri}: #{response.body}"
Expand Down
97 changes: 97 additions & 0 deletions lib/braintrust/internal/http.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# frozen_string_literal: true

require "net/http"
require "uri"
require "zlib"
require "stringio"
require_relative "../logger"

module Braintrust
module Internal
# HTTP utilities for redirect following and response decompression.
# Drop-in enhancement for raw Net::HTTP request calls throughout the SDK.
module Http
DEFAULT_MAX_REDIRECTS = 5

# Execute an HTTP request, following redirects as needed.
#
# @param uri [URI] The request URI
# @param request [Net::HTTPRequest] The prepared request object
# @param max_redirects [Integer] Maximum number of redirects to follow
# @return [Net::HTTPResponse] The final response
# @raise [Braintrust::Error] On too many redirects or missing Location header
def self.with_redirects(uri, request, max_redirects: DEFAULT_MAX_REDIRECTS)
response = perform_request(uri, request)

redirects = 0
original_request = request

while response.is_a?(Net::HTTPRedirection)
redirects += 1
if redirects > max_redirects
raise Error, "Too many redirects (max #{max_redirects})"
end

location = response["location"]
unless location
raise Error, "Redirect response #{response.code} without Location header"
end

redirect_uri = URI(location)
redirect_uri = uri + redirect_uri unless redirect_uri.host

Log.debug("[HTTP] Following #{response.code} redirect to #{redirect_uri}")

request = build_redirect_request(response, redirect_uri, original_request, uri)
uri = redirect_uri
response = perform_request(uri, request)
end

response
end

# Decompress an HTTP response body in place based on Content-Encoding.
# No-op if the response has no recognized encoding.
#
# @param response [Net::HTTPResponse] The response to decompress
# @return [void]
def self.decompress_response!(response)
encoding = response["content-encoding"]&.downcase
case encoding
when "gzip", "x-gzip"
gz = Zlib::GzipReader.new(StringIO.new(response.body))
response.body.replace(gz.read)
gz.close
response.delete("content-encoding")
end
end

def self.perform_request(uri, request)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
http.request(request)
end
private_class_method :perform_request

def self.build_redirect_request(response, redirect_uri, original_request, original_uri)
if response.code == "307" || response.code == "308"
request = original_request.class.new(redirect_uri)
request.body = original_request.body
request["Content-Type"] = original_request["Content-Type"] if original_request["Content-Type"]
else
# 301, 302, 303: follow with GET, no body
request = Net::HTTP::Get.new(redirect_uri)
end

# Strip Authorization when redirecting to a different host (e.g. S3)
if original_uri.host == redirect_uri.host
auth = original_request["Authorization"]
request["Authorization"] = auth if auth
end

request
end
private_class_method :build_redirect_request
end
end
end
4 changes: 3 additions & 1 deletion lib/braintrust/trace/attachment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "net/http"
require_relative "../internal/encoding"
require_relative "../internal/http"
require "uri"

module Braintrust
Expand Down Expand Up @@ -91,7 +92,8 @@ def self.from_file(content_type, path)
# att = Braintrust::Trace::Attachment.from_url("https://example.com/image.png")
def self.from_url(url)
uri = URI.parse(url)
response = Net::HTTP.get_response(uri)
request = Net::HTTP::Get.new(uri)
response = Braintrust::Internal::Http.with_redirects(uri, request)

unless response.is_a?(Net::HTTPSuccess)
raise StandardError, "Failed to fetch URL: #{response.code} #{response.message}"
Expand Down
4 changes: 2 additions & 2 deletions test/braintrust/api/internal/auth_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def test_login_server_error

def test_login_unexpected_response
stub = stub_request(:post, "https://www.braintrust.dev/api/apikey/login")
.to_return(status: 301, body: "", headers: {})
.to_return(status: 100, body: "", headers: {})

error = assert_raises(Braintrust::Error) do
Braintrust::API::Internal::Auth.login(
Expand All @@ -101,7 +101,7 @@ def test_login_unexpected_response
end

assert_match(/unexpected response/i, error.message)
assert_match(/301/, error.message)
assert_match(/100/, error.message)
ensure
remove_request_stub(stub)
end
Expand Down
Loading