Skip to content

Feature request: API to set Host header without affecting dial target #2106

@M09Ic

Description

@M09Ic

Summary

When using fasthttp.Client for many different targets, I would like to be able to override the HTTP Host header (similar to net/http's Request.Host) without changing the dial target / causing DNS resolution for that Host value.

Right now, using SetHost / Header.SetHost appears to influence what address is passed to Client.Dial, so if Host is a domain name it can trigger DNS resolution even if the request URI contains a literal IP. This makes it hard to connect by IP but still send a different Host header.

I have searched existing issues, including #904, #841 and several related discussions, but they do not resolve this specific requirement.

Environment

  • fasthttp version: latest v1.x (via github.com/valyala/fasthttp)
  • Go version: 1.22.x
  • Platform: Linux

Use case

I am building a scanner that sends requests to many different IPs, but for each request I sometimes need to spoof the HTTP Host header (for example: virtual hosts, CDN/WAF frontends, etc.).

Key requirements:

  • Dial target must be the IP from the URL (no DNS for the Host header)
  • I do not want to create a separate HostClient for every possible host string (there can be a lot)
  • I want behavior similar to net/http where:
    • URL controls where we dial (IP/hostname)
    • req.Host only overrides the HTTP Host header

Minimal reproduction

package main

import (
    "errors"
    "fmt"
    "net"
    "time"

    "github.com/valyala/fasthttp"
)

func main() {
    var dialAddrs []string

    client := &fasthttp.Client{
        Dial: func(addr string) (net.Conn, error) {
            dialAddrs = append(dialAddrs, addr)
            // stop before real network I/O
            return nil, errors.New("stop")
        },
    }

    base := "https://127.0.0.1"                      // I want to dial this IP
    hostHeader := "nonexistent-host.invalid"         // placeholder domain, not expected to resolve

    req := fasthttp.AcquireRequest()
    defer fasthttp.ReleaseRequest(req)
    resp := fasthttp.AcquireResponse()
    defer fasthttp.ReleaseResponse(resp)

    req.Header.SetMethod("GET")
    req.SetRequestURI(base + "/test")
    req.SetHost(hostHeader) // or req.Header.SetHost(hostHeader)

    err := client.DoTimeout(req, resp, 2*time.Second)
    fmt.Println("err:", err)

    fmt.Printf("dial addrs: %#v\n", dialAddrs)
}

Typical output (simplified):

err: stop
dial addrs: []string{"nonexistent-host.invalid:443"}

So even though the URL contained 127.0.0.1, the dial target becomes nonexistent-host.invalid:443, which shows that the Host value is used for dial target selection.

Expected / desired behavior

For this use–case I would like:

  • SetRequestURI("https://127.0.0.1/test") to determine the dial target (127.0.0.1:443)
  • SetHost("nonexistent-host.invalid") (or similar) to only change the HTTP Host header
  • The dial target should remain 127.0.0.1:443 and not use the Host value for DNS/dial

This matches how net/http behaves:

req, _ := http.NewRequest("GET", "https://127.0.0.1/test", nil)
req.Host = "nonexistent-host.invalid" // only overrides header
client.Do(req)                         // still dials 127.0.0.1:443

Why existing options don’t fully solve it

Feature request / API idea

Would it be possible to add an API or option that:

  • Lets us set the Host header without changing the dial target, and
  • Keeps current behavior as default for backward compatibility.

For example (just ideas, not a strict proposal):

  • A method like req.Header.SetHostOnly(host string) that only affects the header.
  • Or a flag on Request/Client, e.g. DisableHostHeaderDial or UseURIHostForDial.

I’m happy to adjust my code to any recommended pattern, but right now I don’t see a way to get the net/http–style behavior with a single fasthttp.Client and per–request Host overrides.

Is there an existing pattern I missed, or would you consider adding such an API?

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions