Module: Coolhand::NetHttpInterceptor
- Includes:
- BaseInterceptor
- Defined in:
- lib/coolhand/net_http_interceptor.rb
Defined Under Namespace
Modules: ResponseInterceptor
Constant Summary
Constants included from BaseInterceptor
BaseInterceptor::SENSITIVE_HEADER_PATTERN, BaseInterceptor::SENSITIVE_QUERY_PARAM_PATTERN
Class Method Summary collapse
Instance Method Summary collapse
Methods included from BaseInterceptor
normalize_header_value, parse_json, sanitize_headers, sanitize_url, send_complete_request_log
Class Method Details
.patch! ⇒ Object
27 28 29 30 31 32 33 34 35 |
# File 'lib/coolhand/net_http_interceptor.rb', line 27 def self.patch! return if @patched Net::HTTP.prepend(self) Net::HTTPResponse.prepend(ResponseInterceptor) @patched = true Coolhand.log "🔗 Net::HTTP interceptor patched" end |
.patched? ⇒ Boolean
44 45 46 |
# File 'lib/coolhand/net_http_interceptor.rb', line 44 def self.patched? @patched end |
Instance Method Details
#request(req, body = nil, &block) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/coolhand/net_http_interceptor.rb', line 48 def request(req, body = nil, &block) return super unless NetHttpInterceptor.patched? active = (Thread.current[:coolhand_active_requests] ||= {}.compare_by_identity) return super if active.key?(self) url = build_url_for_request(self, req) return super unless intercept?(url) return super unless should_capture? # Capture body before setting the guard — if this raises we skip logging cleanly # and the guard is never set, so there is no leak. A failure here (e.g. an # already-consumed body_stream) must never prevent the real request below # from being attempted — this gem must never be the reason the host # app's actual LLM call doesn't happen. captured_body = begin capture_request_body(req, body) rescue StandardError => e Coolhand.log "❌ Error capturing request body: #{e.}" nil end active[self] = true start_time = Time.now request_id = SecureRandom.uuid response = nil status_code = nil response_body = nil # Save/restore rather than just nil-ing: a request made from inside # this request's own streaming block (nested interception) would # otherwise clobber this request's in-progress buffer with its own # chunks, mixing one request's content into another's log. previous_stream_buffer = Thread.current[:coolhand_stream_buffer] previous_capturing_stream = Thread.current[:coolhand_capturing_stream] Thread.current[:coolhand_stream_buffer] = nil Thread.current[:coolhand_capturing_stream] = true begin response = super body_content = Thread.current[:coolhand_stream_buffer] || response&.body body_content = body_content.dup.force_encoding("UTF-8") if body_content.is_a?(String) status_code = response.respond_to?(:code) ? response.code.to_i : nil response_body = parse_json(body_content) rescue StandardError => e status_code = extract_status_from_exception(e) response_body = { "error" => { "class" => e.class.name, "message" => e. } } raise ensure active.delete(self) Thread.current[:coolhand_stream_buffer] = previous_stream_buffer Thread.current[:coolhand_capturing_stream] = previous_capturing_stream end_time = Time.now duration_ms = ((end_time - start_time) * 1000).round(2) send_complete_request_log( request_id: request_id, method: req.method, url: url, request_headers: sanitize_headers(req), request_body: captured_body, response_headers: sanitize_headers(response), response_body: response_body, status_code: status_code, start_time: start_time, end_time: end_time, duration_ms: duration_ms, is_streaming: !!block ) end response end |