Skip to content

Commit 44bc270

Browse files
Copilotintel352
andauthored
Fix reverseproxy module service dependency resolution for httpclient (#17)
* Initial plan * Debug service dependency injection: Constructor called but httpclient service missing Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * Fix reverseproxy httpclient service resolution and enable verbose logging demo Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * Add interface-based service dependency resolution with comprehensive tests Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * Simplify service dependency resolution by removing duplicate http-doer service and integration module Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * Fix major linter issues and test failures in httpclient and reverseproxy modules Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * Fix all remaining linter errors and maintain passing tests Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * Fix reverseproxy module test failure by removing featureFlagEvaluator dependency Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * Address review feedback: restore featureFlagEvaluator, simplify httpclient matching, remove custom httpDoer interface Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Co-authored-by: Jonathan Langevin <jlangevin@crisistextline.org>
1 parent 6fc2550 commit 44bc270

13 files changed

Lines changed: 463 additions & 84 deletions

examples/http-client/README.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
# HTTP Client Example
22

3-
This example demonstrates the integration of the HTTP client module with other modules in a reverse proxy setup, showcasing advanced HTTP client features and configuration.
3+
This example demonstrates the integration of the `httpclient` and `reverseproxy` modules, showcasing how the reverseproxy module properly uses the httpclient service for making HTTP requests with verbose logging.
4+
5+
## Features Demonstrated
6+
7+
- **Service Integration**: Shows how the reverseproxy module automatically uses the httpclient service when available
8+
- **Verbose HTTP Logging**: Demonstrates detailed request/response logging through the httpclient service
9+
- **File Logging**: Captures HTTP request/response details to files for analysis
10+
- **Modular Architecture**: Clean separation of concerns between routing (reverseproxy) and HTTP client functionality (httpclient)
11+
- **Service Dependency Resolution**: Example of how modules can depend on services provided by other modules
412

513
## What it demonstrates
614

715
- **HTTP Client Module Integration**: How the httpclient module integrates with other modules
816
- **Advanced HTTP Client Configuration**: Connection pooling, timeouts, and performance tuning
917
- **Reverse Proxy with Custom Client**: Using a configured HTTP client for proxying requests
1018
- **Module Service Dependencies**: How modules can provide services to other modules
11-
- **Verbose Logging Options**: Basic HTTP client logging capabilities
19+
- **Verbose Logging Options**: Advanced HTTP client logging capabilities with file output
1220

1321
## Features
1422

@@ -18,6 +26,7 @@ This example demonstrates the integration of the HTTP client module with other m
1826
- ChiMux router with CORS support
1927
- HTTP server for receiving requests
2028
- Compression and keep-alive settings
29+
- **NEW**: Comprehensive HTTP request/response logging to files
2130

2231
## Running the Example
2332

@@ -39,24 +48,26 @@ The server will start on `localhost:8080` and act as a reverse proxy that uses t
3948
```yaml
4049
httpclient:
4150
# Connection pooling settings
42-
max_idle_conns: 50
43-
max_idle_conns_per_host: 5
44-
idle_conn_timeout: 60
51+
max_idle_conns: 100
52+
max_idle_conns_per_host: 10
53+
idle_conn_timeout: 90
4554

4655
# Timeout settings
47-
request_timeout: 15
48-
tls_timeout: 5
56+
request_timeout: 30
57+
tls_timeout: 10
4958

5059
# Other settings
5160
disable_compression: false
5261
disable_keep_alives: false
5362
verbose: true
5463

55-
# Verbose logging options
64+
# Verbose logging options (enable for demonstration)
5665
verbose_options:
57-
log_headers: false
58-
log_body: false
59-
max_body_log_size: 1024
66+
log_headers: true
67+
log_body: true
68+
max_body_log_size: 2048
69+
log_to_file: true
70+
log_file_path: "./http_client_logs"
6071
```
6172
6273
### Reverse Proxy Integration
@@ -81,6 +92,14 @@ curl http://localhost:8080/proxy/httpbin/headers
8192
curl http://localhost:8080/proxy/httpbin/user-agent
8293
```
8394

95+
## Verification
96+
97+
When the example runs correctly, you should see:
98+
99+
1. **Service Integration Success**: Log message showing `"Using HTTP client from httpclient service"` instead of `"Using default HTTP client (no httpclient service available)"`
100+
2. **Verbose Logging**: Detailed HTTP request/response logs including timing information
101+
3. **File Logging**: HTTP transaction logs saved to the `./http_client_logs` directory
102+
84103
## Key Features Demonstrated
85104

86105
1. **Connection Pooling**: Efficient reuse of HTTP connections
@@ -89,6 +108,7 @@ curl http://localhost:8080/proxy/httpbin/user-agent
89108
4. **Compression Handling**: Configurable request/response compression
90109
5. **Keep-Alive Control**: Connection persistence management
91110
6. **Verbose Logging**: Request/response logging for debugging
111+
7. **File-Based Logging**: Persistent HTTP transaction logs for analysis
92112

93113
## Module Architecture
94114

@@ -99,6 +119,7 @@ HTTP Request → ChiMux Router → ReverseProxy Module → HTTP Client Module
99119
- Connection pooling
100120
- Custom timeouts
101121
- Logging capabilities
122+
- File-based transaction logs
102123
```
103124

104125
## Use Cases
@@ -109,5 +130,6 @@ This example is ideal for:
109130
- Services needing detailed HTTP client monitoring
110131
- Applications with strict timeout requirements
111132
- Systems requiring HTTP client telemetry
133+
- Debugging and troubleshooting HTTP integrations
112134

113135
The HTTP client module provides enterprise-grade HTTP client functionality that can be shared across multiple modules in your application.

examples/http-client/config.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ httpclient:
3333
disable_keep_alives: false
3434
verbose: true
3535

36-
# Verbose logging options
36+
# Verbose logging options (enable for demonstration)
3737
verbose_options:
38-
log_headers: false
39-
log_body: false
40-
max_body_log_size: 1024
38+
log_headers: true
39+
log_body: true
40+
max_body_log_size: 2048
41+
log_to_file: true
42+
log_file_path: "./http_client_logs"
4143

4244
# HTTP Server configuration
4345
httpserver:

modules/httpclient/config.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
package httpclient
33

44
import (
5+
"errors"
56
"fmt"
67
"time"
78
)
89

10+
var (
11+
// ErrLogFilePathRequired is returned when log_to_file is enabled but log_file_path is not specified
12+
ErrLogFilePathRequired = errors.New("log_file_path must be specified when log_to_file is enabled")
13+
)
14+
915
// Config defines the configuration for the HTTP client module.
1016
// This structure contains all the settings needed to configure HTTP client
1117
// behavior, connection pooling, timeouts, and logging.
@@ -160,7 +166,7 @@ func (c *Config) Validate() error {
160166

161167
// Validate verbose log file path if logging to file is enabled
162168
if c.Verbose && c.VerboseOptions != nil && c.VerboseOptions.LogToFile && c.VerboseOptions.LogFilePath == "" {
163-
return fmt.Errorf("log_file_path must be specified when log_to_file is enabled")
169+
return fmt.Errorf("config validation error: %w", ErrLogFilePathRequired)
164170
}
165171

166172
return nil

modules/httpclient/logger.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,19 @@ func NewFileLogger(baseDir string, logger modular.Logger) (*FileLogger, error) {
5050
// LogRequest writes request data to a file.
5151
func (f *FileLogger) LogRequest(id string, data []byte) error {
5252
requestFile := filepath.Join(f.requestDir, fmt.Sprintf("request_%s_%d.log", id, time.Now().UnixNano()))
53-
return os.WriteFile(requestFile, data, 0644)
53+
if err := os.WriteFile(requestFile, data, 0600); err != nil {
54+
return fmt.Errorf("failed to write request log file %s: %w", requestFile, err)
55+
}
56+
return nil
5457
}
5558

5659
// LogResponse writes response data to a file.
5760
func (f *FileLogger) LogResponse(id string, data []byte) error {
5861
responseFile := filepath.Join(f.responseDir, fmt.Sprintf("response_%s_%d.log", id, time.Now().UnixNano()))
59-
return os.WriteFile(responseFile, data, 0644)
62+
if err := os.WriteFile(responseFile, data, 0600); err != nil {
63+
return fmt.Errorf("failed to write response log file %s: %w", responseFile, err)
64+
}
65+
return nil
6066
}
6167

6268
// LogTransactionToFile logs both request and response data to a single file for easier analysis.
@@ -83,19 +89,19 @@ func (f *FileLogger) LogTransactionToFile(id string, reqData, respData []byte, d
8389

8490
// Write transaction metadata
8591
if _, err := fmt.Fprintf(file, "Transaction ID: %s\n", id); err != nil {
86-
return err
92+
return fmt.Errorf("failed to write transaction ID to log file: %w", err)
8793
}
8894
if _, err := fmt.Fprintf(file, "URL: %s\n", url); err != nil {
89-
return err
95+
return fmt.Errorf("failed to write URL to log file: %w", err)
9096
}
9197
if _, err := fmt.Fprintf(file, "Time: %s\n", time.Now().Format(time.RFC3339)); err != nil {
92-
return err
98+
return fmt.Errorf("failed to write timestamp to log file: %w", err)
9399
}
94100
if _, err := fmt.Fprintf(file, "Duration: %d ms\n", duration.Milliseconds()); err != nil {
95-
return err
101+
return fmt.Errorf("failed to write duration to log file: %w", err)
96102
}
97103
if _, err := fmt.Fprintf(file, "\n----- REQUEST -----\n\n"); err != nil {
98-
return err
104+
return fmt.Errorf("failed to write request separator to log file: %w", err)
99105
}
100106

101107
// Write request data
@@ -105,7 +111,7 @@ func (f *FileLogger) LogTransactionToFile(id string, reqData, respData []byte, d
105111

106112
// Write response data with a separator
107113
if _, err := fmt.Fprintf(file, "\n\n----- RESPONSE -----\n\n"); err != nil {
108-
return err
114+
return fmt.Errorf("failed to write response separator to log file: %w", err)
109115
}
110116
if _, err := file.Write(respData); err != nil {
111117
return fmt.Errorf("failed to write response data: %w", err)

modules/httpclient/module.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,13 @@ func (m *HTTPClientModule) ProvidesServices() []modular.ServiceProvider {
327327
return []modular.ServiceProvider{
328328
{
329329
Name: ServiceName,
330-
Description: "HTTP client service for making HTTP requests",
331-
Instance: m,
330+
Description: "HTTP client (*http.Client) for direct usage",
331+
Instance: m.httpClient, // Provide the actual *http.Client instance
332332
},
333333
{
334-
Name: "http.Client",
335-
Description: "HTTP client service for making HTTP requests",
336-
Instance: m.httpClient,
334+
Name: "httpclient-service",
335+
Description: "HTTP client service interface (ClientService) for advanced features",
336+
Instance: m, // Provide the service interface for modules that need additional features
337337
},
338338
}
339339
}
@@ -431,7 +431,7 @@ func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
431431
"url", req.URL.String(),
432432
"error", err,
433433
)
434-
return resp, err
434+
return resp, fmt.Errorf("http request failed: %w", err)
435435
}
436436

437437
// Log the response
@@ -479,7 +479,10 @@ func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
479479
}
480480
}
481481

482-
return resp, err
482+
if err != nil {
483+
return resp, fmt.Errorf("http request completion failed: %w", err)
484+
}
485+
return resp, nil
483486
}
484487

485488
// logRequest logs detailed information about the request.

modules/httpclient/module_test.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package httpclient
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
67
"net/http/httptest"
78
"os"
@@ -11,6 +12,7 @@ import (
1112
"github.com/CrisisTextLine/modular"
1213
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/mock"
15+
"github.com/stretchr/testify/require"
1416
)
1517

1618
// MockApplication implements modular.Application interface for testing
@@ -20,7 +22,10 @@ type MockApplication struct {
2022

2123
func (m *MockApplication) GetConfigSection(name string) (modular.ConfigProvider, error) {
2224
args := m.Called(name)
23-
return args.Get(0).(modular.ConfigProvider), args.Error(1)
25+
if err := args.Error(1); err != nil {
26+
return args.Get(0).(modular.ConfigProvider), fmt.Errorf("failed to get config section %s: %w", name, err)
27+
}
28+
return args.Get(0).(modular.ConfigProvider), nil
2429
}
2530

2631
func (m *MockApplication) RegisterConfigSection(name string, provider modular.ConfigProvider) {
@@ -53,12 +58,18 @@ func (m *MockApplication) ConfigSections() map[string]modular.ConfigProvider {
5358

5459
func (m *MockApplication) RegisterService(name string, service any) error {
5560
args := m.Called(name, service)
56-
return args.Error(0)
61+
if err := args.Error(0); err != nil {
62+
return fmt.Errorf("failed to register service %s: %w", name, err)
63+
}
64+
return nil
5765
}
5866

5967
func (m *MockApplication) GetService(name string, target any) error {
6068
args := m.Called(name, target)
61-
return args.Error(0)
69+
if err := args.Error(0); err != nil {
70+
return fmt.Errorf("failed to get service %s: %w", name, err)
71+
}
72+
return nil
6273
}
6374

6475
// Add other required methods to satisfy the interface
@@ -151,7 +162,7 @@ func TestHTTPClientModule_Init(t *testing.T) {
151162
err := module.Init(mockApp)
152163

153164
// Assertions
154-
assert.NoError(t, err, "Init should not return an error")
165+
require.NoError(t, err, "Init should not return an error")
155166
assert.NotNil(t, module.httpClient, "HTTP client should not be nil")
156167
assert.Equal(t, 30*time.Second, module.httpClient.Timeout, "Timeout should be set correctly")
157168

@@ -205,7 +216,7 @@ func TestHTTPClientModule_RequestModifier(t *testing.T) {
205216
}
206217

207218
// Create a test request
208-
req, _ := http.NewRequest("GET", "http://example.com", nil)
219+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://example.com", nil)
209220

210221
// Apply the modifier
211222
modifiedReq := module.RequestModifier()(req)
@@ -228,7 +239,7 @@ func TestHTTPClientModule_SetRequestModifier(t *testing.T) {
228239
})
229240

230241
// Create a test request
231-
req, _ := http.NewRequest("GET", "http://example.com", nil)
242+
req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://example.com", nil)
232243

233244
// Apply the modifier
234245
modifiedReq := module.modifier(req)
@@ -254,7 +265,7 @@ func TestHTTPClientModule_LoggingTransport(t *testing.T) {
254265
}()
255266

256267
fileLogger, err := NewFileLogger(tmpDir, mockLogger)
257-
assert.NoError(t, err)
268+
require.NoError(t, err)
258269

259270
// Setup test server
260271
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -286,11 +297,11 @@ func TestHTTPClientModule_LoggingTransport(t *testing.T) {
286297
}
287298

288299
// Make a request
289-
req, _ := http.NewRequest("GET", server.URL, nil)
300+
req, _ := http.NewRequestWithContext(context.Background(), "GET", server.URL, nil)
290301
resp, err := client.Do(req)
291302

292303
// Assertions
293-
assert.NoError(t, err, "Request should not fail")
304+
require.NoError(t, err, "Request should not fail")
294305
assert.NotNil(t, resp, "Response should not be nil")
295306
assert.Equal(t, http.StatusOK, resp.StatusCode, "Status code should be 200")
296307

@@ -332,14 +343,14 @@ func TestHTTPClientModule_IntegrationWithServer(t *testing.T) {
332343
}
333344

334345
// Create request
335-
req, _ := http.NewRequest("GET", server.URL, nil)
346+
req, _ := http.NewRequestWithContext(context.Background(), "GET", server.URL, nil)
336347

337348
// Apply modifier and make the request
338349
req = module.RequestModifier()(req)
339350
resp, err := module.Client().Do(req)
340351

341352
// Assertions
342-
assert.NoError(t, err, "Request should not fail")
353+
require.NoError(t, err, "Request should not fail")
343354
assert.NotNil(t, resp, "Response should not be nil")
344355
assert.Equal(t, http.StatusOK, resp.StatusCode, "Status code should be 200")
345356
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"), "Content-Type should be application/json")

modules/httpclient/service.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ import (
44
"net/http"
55
)
66

7+
// HTTPDoer defines the minimal interface for making HTTP requests.
8+
// This interface is implemented by http.Client and provides a simple
9+
// abstraction for modules that only need to make HTTP requests without
10+
// the additional features provided by ClientService.
11+
//
12+
// Use this interface when you only need to make HTTP requests:
13+
//
14+
// type MyModule struct {
15+
// httpClient HTTPDoer
16+
// }
17+
//
18+
// func (m *MyModule) RequiresServices() []modular.ServiceDependency {
19+
// return []modular.ServiceDependency{
20+
// {
21+
// Name: "http-doer",
22+
// Required: true,
23+
// MatchByInterface: true,
24+
// SatisfiesInterface: reflect.TypeOf((*HTTPDoer)(nil)).Elem(),
25+
// },
26+
// }
27+
// }
28+
type HTTPDoer interface {
29+
Do(req *http.Request) (*http.Response, error)
30+
}
31+
732
// ClientService defines the interface for the HTTP client service.
833
// This interface provides access to configured HTTP clients and request
934
// modification capabilities. Any module that needs to make HTTP requests

0 commit comments

Comments
 (0)