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
248 changes: 248 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
<a id="readme-top"></a>

[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![Apache 2.0 License][license-shield]][license-url]

<br />
<div align="center">
<h1>httpfx</h1>
<p>
Uber Fx module for <code>net/http.Client</code> with SOCKS5, HTTP proxy, and environment-based proxy support.
</p>
<a href="https://github.com/go-core-fx/httpfx/issues/new">Report Bug</a>
&middot;
<a href="https://github.com/go-core-fx/httpfx/issues/new">Request Feature</a>
</div>

## Table of Contents
- [Table of Contents](#table-of-contents)
- [About The Project](#about-the-project)
- [Built With](#built-with)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Module Setup](#module-setup)
- [Configuration Reference](#configuration-reference)
- [Factory \& Per-Client Options](#factory--per-client-options)
- [Available Options](#available-options)
- [Proxy Examples](#proxy-examples)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
- [Acknowledgments](#acknowledgments)


---

## About The Project

`httpfx` is an [Uber Fx](https://uber-go.github.io/fx/) module that provides a configured `*http.Client` and a `Factory` for creating additional client instances. It supports:

- **SOCKS5 proxy** via `golang.org/x/net/proxy` (`socks5://user:pass@host:port`)
- **HTTP-level proxy** via `net/http.Transport.Proxy` (`http://proxy:8080`)
- **Environment-based proxy** via `ALL_PROXY` env var
- **Per-host proxy bypass** for hosts that should connect directly
- **Per-client overrides** via functional options on the `Factory`
- **Transport tuning** — idle connections, timeouts, pool sizes

### Built With

- [![Go](https://img.shields.io/badge/Go-00ADD8?style=for-the-badge&logo=go&logoColor=white)](https://go.dev/)
- [![Uber Fx](https://img.shields.io/badge/Uber%20Fx-000000?style=for-the-badge)](https://uber-go.github.io/fx/)
- [![x/net](https://img.shields.io/badge/golang.org%2Fx%2Fnet-000000?style=for-the-badge)](https://pkg.go.dev/golang.org/x/net/proxy)

<p align="right">(<a href="#readme-top">back to top</a>)</p>

---

## Getting Started

### Prerequisites

- Go 1.25+
- An application using [Uber Fx](https://uber-go.github.io/fx/) for dependency injection

### Installation

```sh
go get github.com/go-core-fx/httpfx@latest
```

<p align="right">(<a href="#readme-top">back to top</a>)</p>

---

## Usage

### Module Setup

```go
import (
"time"

"github.com/go-core-fx/httpfx"
"go.uber.org/fx"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func main() {
fx.New(
fx.Provide(func() httpfx.Config {
return httpfx.Config{
ProxyURL: "socks5://127.0.0.1:1080",
Bypass: "localhost,127.0.0.1",
Timeout: 30 * time.Second,
}
}),
httpfx.Module(),
// ... other modules
).Run()
}
```

The module provides both a default `*http.Client` and a `Factory` for creating additional clients.

### Configuration Reference

| Field | Type | Default | Description |
| --------------------- | --------------- | ------- | --------------------------------------------------------------------------------- |
| `ProxyURL` | `string` | `""` | SOCKS5 proxy URL (e.g. `socks5://user:pass@host:port`). Takes highest precedence. |
| `ProxyFromEnv` | `bool` | `false` | Read proxy from `ALL_PROXY` env var when `ProxyURL` is empty. |
| `Bypass` | `string` | `""` | Comma-separated hosts to bypass the SOCKS proxy (e.g. `localhost,127.0.0.1`). |
| `Timeout` | `time.Duration` | `0` | Client-level request timeout. Zero means no timeout. |
| `MaxIdleConns` | `int` | `0` | Maximum idle (keep-alive) connections. Zero means no limit. |
| `MaxIdleConnsPerHost` | `int` | `0` | Maximum idle connections per host. Zero means Go default (2). |
| `IdleConnTimeout` | `time.Duration` | `0` | Maximum time a connection stays idle. Zero means no timeout. |

**Proxy precedence:** `ProxyURL` → `ProxyFromEnv`. To disable all proxying, clear `ProxyURL` and set `ProxyFromEnv: false`.

### Factory & Per-Client Options

Inject `httpfx.Factory` to create additional clients with shared base config but per-client overrides:

```go
func Handler(f httpfx.Factory) {
// Default client — uses factory base config
defaultClient := f.NewClient()

// Override timeout for a fast endpoint
apiClient := f.NewClient(httpfx.WithTimeout(5 * time.Second))

// Disable proxy for internal service calls
internalClient := f.NewClient(httpfx.WithProxyURL(""))

// Custom transport for a specific module
uploadClient := f.NewClient(
httpfx.WithTimeout(5 * time.Minute),
httpfx.WithMaxIdleConns(10),
)
}
```

#### Available Options

| Option | Description |
| ---------------------------- | -------------------------------------- |
| `WithProxyURL(url)` | Override SOCKS5 proxy URL |
| `WithProxyFromEnv(v)` | Override env-based proxy flag |
| `WithBypass(bypass)` | Override proxy bypass list |
| `WithTimeout(d)` | Override client timeout |
| `WithMaxIdleConns(n)` | Override max idle connections |
| `WithMaxIdleConnsPerHost(n)` | Override max idle connections per host |
| `WithIdleConnTimeout(d)` | Override idle connection timeout |

### Proxy Examples

**SOCKS5 with authentication:**

```go
httpfx.Config{
ProxyURL: "socks5://user:pass@127.0.0.1:1080",
}
```

**Environment-based (reads `ALL_PROXY`):**

```go
httpfx.Config{
ProxyFromEnv: true,
}
```

```sh
export ALL_PROXY="socks5://127.0.0.1:1080"
```

**SOCKS5 with bypass for local addresses and CIDR ranges:**

```go
httpfx.Config{
ProxyURL: "socks5://127.0.0.1:1080",
Bypass: "localhost,127.0.0.1,192.168.0.0/16",
}
```

<p align="right">(<a href="#readme-top">back to top</a>)</p>

---

## Roadmap

- [x] SOCKS5 proxy support via `golang.org/x/net/proxy`
- [x] Environment-based proxy (`ALL_PROXY`)
- [x] Per-host proxy bypass
- [x] Factory with per-client functional options
- [x] Transport tuning (idle connections, timeouts)
- [ ] TLS configuration options
- [ ] Proxy authentication header support

See the [open issues](https://github.com/go-core-fx/httpfx/issues) for a full list of proposed features.

<p align="right">(<a href="#readme-top">back to top</a>)</p>

---

## Contributing

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.

1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request

<p align="right">(<a href="#readme-top">back to top</a>)</p>

---

## License

Distributed under the Apache License 2.0. See `LICENSE` for more information.

<p align="right">(<a href="#readme-top">back to top</a>)</p>

---

## Acknowledgments

- [Uber Fx](https://uber-go.github.io/fx/) — dependency injection framework
- [golang.org/x/net/proxy](https://pkg.go.dev/golang.org/x/net/proxy) — SOCKS5 and environment-based proxy dialers
- [Best-README-Template](https://github.com/othneildrew/Best-README-Template) — README structure

<p align="right">(<a href="#readme-top">back to top</a>)</p>

<!-- MARKDOWN LINKS & IMAGES -->
[contributors-shield]: https://img.shields.io/github/contributors/go-core-fx/httpfx.svg?style=for-the-badge
[contributors-url]: https://github.com/go-core-fx/httpfx/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/go-core-fx/httpfx.svg?style=for-the-badge
[forks-url]: https://github.com/go-core-fx/httpfx/network/members
[stars-shield]: https://img.shields.io/github/stars/go-core-fx/httpfx.svg?style=for-the-badge
[stars-url]: https://github.com/go-core-fx/httpfx/stargazers
[issues-shield]: https://img.shields.io/github/issues/go-core-fx/httpfx.svg?style=for-the-badge
[issues-url]: https://github.com/go-core-fx/httpfx/issues
[license-shield]: https://img.shields.io/github/license/go-core-fx/httpfx.svg?style=for-the-badge
[license-url]: https://github.com/go-core-fx/httpfx/blob/master/LICENSE
107 changes: 107 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package httpfx

import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"

"golang.org/x/net/proxy"
)

func newClient(config Config) (*http.Client, error) {
transport := &http.Transport{
MaxIdleConns: config.MaxIdleConns,
MaxIdleConnsPerHost: config.MaxIdleConnsPerHost,
IdleConnTimeout: config.IdleConnTimeout,
}

if err := applyProxy(transport, config); err != nil {
return nil, err
}

return &http.Client{
Transport: transport,
Timeout: config.Timeout,
}, nil
}

func applyProxy(transport *http.Transport, config Config) error {
switch {
case config.ProxyURL != "":
return applySOCKSProxy(transport, config.ProxyURL, config.Bypass)
case config.ProxyFromEnv:
return applyEnvProxy(transport, config.Bypass)
default:
return nil
}
}

func applySOCKSProxy(transport *http.Transport, rawURL, bypass string) error {
u, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("%w", ErrInvalidProxyURL)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if u.Hostname() == "" {
return fmt.Errorf("%w: empty hostname", ErrInvalidProxyURL)
}

dialer, err := proxy.FromURL(u, proxy.Direct)
if err != nil {
return fmt.Errorf("%w: %w", ErrProxyDialFailed, err)
Comment thread
capcom6 marked this conversation as resolved.
}

dialer = applyBypass(dialer, bypass)
setTransportDialer(transport, dialer)

return nil
}

func applyEnvProxy(transport *http.Transport, bypass string) error {
hasProxyEnv := os.Getenv("ALL_PROXY") != "" || os.Getenv("all_proxy") != ""

dialer := proxy.FromEnvironment()

if dialer == proxy.Direct {
if hasProxyEnv {
return fmt.Errorf("%w: proxy environment variable set but invalid", ErrInvalidProxyURL)
}
return nil
}
Comment thread
capcom6 marked this conversation as resolved.

dialer = applyBypass(dialer, bypass)
setTransportDialer(transport, dialer)

return nil
}

func applyBypass(dialer proxy.Dialer, bypass string) proxy.Dialer {
bypass = strings.TrimSpace(bypass)
if bypass == "" {
return dialer
}

perHost := proxy.NewPerHost(dialer, proxy.Direct)
perHost.AddFromString(bypass)

return perHost
}

func setTransportDialer(transport *http.Transport, dialer proxy.Dialer) {
if cd, ok := dialer.(proxy.ContextDialer); ok {
transport.DialContext = cd.DialContext
return
}

transport.DialContext = func(
_ context.Context,
network,
addr string,
) (net.Conn, error) {
return dialer.Dial(network, addr)
}
}
33 changes: 33 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package httpfx

import "time"

// Config holds the HTTP client configuration.
type Config struct {
// ProxyURL is an explicit SOCKS5 proxy URL (e.g., "socks5://user:pass@host:port").
// Empty means no proxy. Takes precedence over ProxyFromEnv.
ProxyURL string

// ProxyFromEnv enables reading the proxy from the ALL_PROXY environment variable.
// Used only when ProxyURL is empty.
ProxyFromEnv bool

// Bypass is a comma-separated list of hosts that bypass the proxy
// (e.g., "localhost,127.0.0.1").
Bypass string
Comment thread
capcom6 marked this conversation as resolved.

// Timeout is the HTTP client-level timeout. Zero means no timeout.
Timeout time.Duration

// MaxIdleConns is the maximum number of idle (keep-alive) connections.
// Zero means no limit.
MaxIdleConns int

// MaxIdleConnsPerHost is the maximum idle connections per host.
// Zero means DefaultMaxIdleConnsPerHost (2).
MaxIdleConnsPerHost int

// IdleConnTimeout is the maximum time an idle connection is kept alive.
// Zero means no timeout.
IdleConnTimeout time.Duration
}
Loading
Loading