-
Notifications
You must be signed in to change notification settings - Fork 998
Description
Convert Env to BaseOptions (Preserve Middleware API)
Phase: 4 - Env Conversion (CRITICAL)
Release Target: v2.x.3
Tracking Issue: #1647
RFC: docs/options-detach-plan.md
Overview
Convert Env to inherit from BaseOptions while absolutely preserving the hash-like [] and []= API that middleware depends on. Env is the heart of Faraday's request/response cycle and breaking it would break the entire ecosystem.
Why This Is Critical
- Middleware Everywhere: All middleware uses
env[:url],env[:body],env[:request_headers], etc. - Third-Party Ecosystem: External adapter and middleware gems depend on this API
- Custom Middleware: Users' custom middleware expects hash-like access
- No Warnings Possible: We can't deprecate this - must work perfectly
Current Structure
File: lib/faraday/options/env.rb (204 lines)
- Inherits from
Options - 13 core members:
:method, :request_body, :url, :request, :request_headers, :ssl, :parallel_manager, :params, :response, :response_headers, :response_body, :status, :reason_phrase - Custom members via
custom_members(dynamic attributes) - Hash-like access via
[]and[]= - Many helper methods:
body/body=(delegates to request_body or response_body)success?,needs_body?,parse_body?parallel?,stream_response?in_member_set?,clear_body
New Structure
# frozen_string_literal: true
module Faraday
class Env < BaseOptions
MEMBERS = %i[
method request_body url request request_headers ssl parallel_manager
params response response_headers response_body status reason_phrase
].freeze
COERCIONS = {
request: RequestOptions,
request_headers: Utils::Headers,
response_headers: Utils::Headers,
ssl: SSLOptions
}.freeze
attr_accessor :method, :request_body, :url, :request, :request_headers,
:ssl, :parallel_manager, :params, :response, :response_headers,
:response_body, :status, :reason_phrase
def initialize(options = {})
super(options)
@custom_members = {}
# Handle any custom members from initialization
options.each do |key, value|
@custom_members[key.to_sym] = value unless self.class::MEMBERS.include?(key.to_sym)
end
end
# CRITICAL: Preserve hash-like access for middleware compatibility
def [](key)
key = key.to_sym
if self.class::MEMBERS.include?(key)
instance_variable_get(:"@#{key}")
else
@custom_members[key]
end
end
def []=(key, value)
key = key.to_sym
if self.class::MEMBERS.include?(key)
# Apply coercion if needed
value = coerce(key, value) if self.class::COERCIONS[key]
instance_variable_set(:"@#{key}", value)
else
@custom_members[key] = value
end
end
# Body delegation
def body
response_body || request_body
end
def body=(value)
if response
@response_body = value
else
@request_body = value
end
end
# Helper methods
def success?
status && status >= 200 && status < 300
end
def needs_body?
!response && %i[post put patch].include?(method)
end
def parse_body?
!!response && !response_body && !stream_response?
end
def parallel?
!!parallel_manager
end
def stream_response?
request&.stream_response?
end
def clear_body
request_body = nil
response_body = nil
end
def to_hash
hash = super
hash.merge!(@custom_members) if @custom_members
hash
end
def deep_dup
dup = super
dup.instance_variable_set(:@custom_members, @custom_members.dup)
dup
end
private
def in_member_set?(key)
self.class::MEMBERS.include?(key.to_sym) || @custom_members.key?(key.to_sym)
end
end
endImplementation Tasks
Core Conversion
- Update class to inherit from
BaseOptions - Define
MEMBERSandCOERCIONSconstants - Add explicit
attr_accessorfor all core members - PRESERVE
[]and[]=methods - Handle
custom_membersfor dynamic attributes - Preserve all helper methods
Body Handling
- Preserve
body/body=delegation logic - Preserve
clear_bodymethod - Test request_body vs response_body scenarios
Coercion
- Ensure
requestcoerces toRequestOptions - Ensure
request_headers/response_headerscoerce toUtils::Headers - Ensure
sslcoerces toSSLOptions
Custom Members
- Test adding custom members via initialization
- Test adding custom members via
[]= - Test accessing custom members via
[] - Ensure
to_hashincludes custom members
Testing
- Update tests in
spec/faraday/options/env_spec.rb - Extensive middleware compatibility tests
- Test all helper methods
- Test body delegation
- Test custom members
- Run FULL integration test suite
- Test with real middleware stack
Critical Middleware Compatibility Tests
Must verify these common middleware patterns work:
# Pattern 1: Reading from env
env[:url]
env[:method]
env[:request_headers]
# Pattern 2: Writing to env
env[:custom_data] = 'value'
env[:request_headers]['Authorization'] = 'Bearer token'
# Pattern 3: Checking existence
env[:custom_key] # returns nil if not present
# Pattern 4: Modifying request
env[:body] = JSON.generate(data)
env[:url].query = new_params
# Pattern 5: Reading response
env[:status]
env[:response_body]
env[:response_headers]Files to Modify
lib/faraday/options/env.rbspec/faraday/options/env_spec.rb
Files to Review (Extensive Integration Testing)
- All middleware in
lib/faraday/middleware/ - All adapters (particularly how they use env)
lib/faraday/connection.rb(env creation and usage)spec/faraday/connection_spec.rbspec/support/shared_examples/request_method.rb- Integration test suite from Add comprehensive integration tests using faraday-live approach #1648
Acceptance Criteria
- Env inherits from BaseOptions
- ALL middleware hash-like access patterns work
- All helper methods preserved
- Body delegation works correctly
- Custom members work (add/access via
[]/[]=) - Nested coercion works (request, ssl, headers)
- All tests pass (unit + integration)
- No breaking changes detected
- Third-party middleware would continue working
Dependencies
- Depends on: Convert ProxyOptions, RequestOptions, SSLOptions to BaseOptions #1651 (RequestOptions, SSLOptions must be converted)
- Depends on: Convert ConnectionOptions to BaseOptions #1652 (ConnectionOptions must be converted)
- Blocks: Convert Env to BaseOptions (preserve middleware API) #1653 (Documentation can only happen after this)
Backward Compatibility
ABSOLUTE REQUIREMENT: All existing middleware must work unchanged.
Preserved APIs:
[]and[]=for hash-like access- All helper methods (success?, needs_body?, etc.)
- Body delegation
- Custom members
- Nested coercion
NO BREAKING CHANGES ALLOWED
Risk Assessment
Risk Level: CRITICAL - HIGHEST
Risks:
- Breaking middleware ecosystem
- Subtle behavior changes in hash access
- Custom members not working
- Body delegation edge cases
- Third-party adapter incompatibility
Mitigation:
- EXTENSIVE testing before releasing
- Test with multiple real middleware examples
- Test with external adapters if possible
- Consider beta release for community testing
- Monitor issue reports closely after release
Testing Strategy
- Unit tests: All Env methods and edge cases
- Integration tests: Full request/response cycles
- Middleware tests: Real middleware stack scenarios
- Compatibility tests: Simulate third-party middleware patterns
- Performance tests: Ensure no significant slowdown
Recommended Testing Period
After implementation, run in testing/staging environment for at least 1-2 weeks before releasing v2.x.3 to production.
Consider:
- Beta gem release (v2.x.3.beta1)
- Community testing period
- Monitor GitHub issues for any reports