From 620c766e6a99b58d6f2ea2b31fb9f4563d2aeb13 Mon Sep 17 00:00:00 2001 From: Misha <6481198+remdev@users.noreply.github.com> Date: Wed, 29 Apr 2026 01:51:34 +0300 Subject: [PATCH 1/2] feat(client): extra headers and optional http/1.1 default transport Integrators can set ExtraHeaders after mandatory MS-ASHTTP headers and opt into disabling HTTP/2 ALPN when using the library-built HTTP client, which helps Exchange deployments that mishandle HTTP/2. --- README.md | 22 ++++++ client/client.go | 65 +++++++++++++--- client/client_extra_test.go | 49 ++++++++++++ client/client_profile_test.go | 142 ++++++++++++++++++++++++++++++++++ client/headers.go | 27 +++++++ client/headers_test.go | 51 ++++++++++++ client/integration_test.go | 40 ++++++++++ internal/spec/coverage.csv | 4 + 8 files changed, 391 insertions(+), 9 deletions(-) create mode 100644 client/client_profile_test.go diff --git a/README.md b/README.md index f723279..22dcbc8 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,28 @@ if eas.PingHasChanges(resp.Status) { } ``` +### Outlook-like client profile + +Many servers key off `DeviceType` and `Locale` (LCID), and expect additional metadata via headers rather than MS-ASHTTP query fields. Use `DeviceType: "Outlook"` when emulating Outlook; set `Locale` to `0x0409` for en-US or `0x0419` for ru-RU. Device model, OS version, or other vendor-specific strings are not separate `Config` fields—supply them with `ExtraHeaders` so they merge after the mandatory headers without replacing `User-Agent`, `MS-ASProtocolVersion`, and other values the client sets. If you must avoid HTTP/2 to match an older appliance or proxy, pass `ForceHTTP11: true` with `HTTPClient: nil`; if you inject your own `HTTPClient`, tune its transport yourself (`ForceHTTP11` is ignored). + +```go +import "net/http" + +_, err := client.New(client.Config{ + BaseURL: ad.URL, + Auth: &client.BasicAuth{Username: "user@example.com", Password: "pass"}, + DeviceID: "stable-device-id", + DeviceType: "Outlook", + Locale: 0x0409, + UserAgent: "Microsoft Office/16.0 (Windows NT 10.0; Microsoft Outlook 16.0.1)", + ExtraHeaders: http.Header{ + "X-MS-Device-MachineName": []string{"WORKSTATION1"}, + "X-OS-Type": []string{"Windows"}, + }, + ForceHTTP11: true, +}) +``` + Runnable end-to-end programs live under [`examples/`](examples/): [`login`](examples/login), [`inbox-sync`](examples/inbox-sync), [`calendar-sync`](examples/calendar-sync), [`ping`](examples/ping). diff --git a/client/client.go b/client/client.go index 5596bcc..0c0a09d 100644 --- a/client/client.go +++ b/client/client.go @@ -3,6 +3,7 @@ package client import ( "bytes" "context" + "crypto/tls" "errors" "fmt" "io" @@ -27,20 +28,50 @@ type Client struct { ProtocolVersion string AcceptLanguage string + // ExtraHeaders are merged into each request after mandatory headers without + // overwriting keys already set (see Config.ExtraHeaders). + ExtraHeaders http.Header + + // ForceHTTP11 reflects the config flag; when HTTPClient was supplied to New + // the transport is never altered and this bit is informational only. + ForceHTTP11 bool + PolicyStore PolicyStore SyncStateStore SyncStateStore } // Config bundles the values required to construct a Client. type Config struct { - BaseURL string - HTTPClient *http.Client - Auth Authenticator - DeviceID string - DeviceType string - UserAgent string - Locale uint16 + BaseURL string + HTTPClient *http.Client + Auth Authenticator + + DeviceID string + // DeviceType is the device class in the MS-ASHTTP query (e.g. "SmartPhone"). + // For an Outlook-style profile many servers expect DeviceType "Outlook". + DeviceType string + + // UserAgent is sent as the mandatory User-Agent header. + UserAgent string + + // Locale is the LCID placed in the binary query (little-endian uint16), for + // example 0x0409 (en-US) or 0x0419 (ru-RU). + Locale uint16 + AcceptLanguage string + + // ExtraHeaders optional integrator headers (device model, OS, or other + // vendor expectations). They are merged after mandatory headers and never + // replace keys the client already set; device model/OS are not separate + // Config fields because MS-ASHTTP only standardizes the query DeviceType. + ExtraHeaders http.Header + + // ForceHTTP11, when true and HTTPClient is nil, builds an HTTP client whose + // transport clones http.DefaultTransport and disables HTTP/2 by setting + // TLSNextProto to a non-nil empty map. When HTTPClient is non-nil, + // ForceHTTP11 is ignored and the caller's transport is not modified. + ForceHTTP11 bool + PolicyStore PolicyStore SyncStateStore SyncStateStore } @@ -59,7 +90,6 @@ func New(cfg Config) (*Client, error) { } c := &Client{ BaseURL: cfg.BaseURL, - HTTPClient: cfg.HTTPClient, Auth: cfg.Auth, DeviceID: cfg.DeviceID, DeviceType: cfg.DeviceType, @@ -67,10 +97,26 @@ func New(cfg Config) (*Client, error) { Locale: cfg.Locale, ProtocolVersion: eas.ProtocolVersion, AcceptLanguage: cfg.AcceptLanguage, + ForceHTTP11: cfg.ForceHTTP11, PolicyStore: cfg.PolicyStore, SyncStateStore: cfg.SyncStateStore, } - if c.HTTPClient == nil { + if len(cfg.ExtraHeaders) > 0 { + c.ExtraHeaders = cfg.ExtraHeaders.Clone() + } + switch { + case cfg.HTTPClient != nil: + c.HTTPClient = cfg.HTTPClient + case cfg.ForceHTTP11: + dt, ok := http.DefaultTransport.(*http.Transport) + if !ok { + c.HTTPClient = http.DefaultClient + break + } + tr := dt.Clone() + tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + c.HTTPClient = &http.Client{Transport: tr} + default: c.HTTPClient = http.DefaultClient } if c.UserAgent == "" { @@ -163,6 +209,7 @@ func (c *Client) doOnce(ctx context.Context, cmd byte, user string, request, res PolicyKey: policyKey, AcceptLanguage: c.AcceptLanguage, }) + mergeExtraHeaders(req.Header, c.ExtraHeaders) if c.Auth != nil { if err := c.Auth.Apply(req); err != nil { return fmt.Errorf("client: auth: %w", err) diff --git a/client/client_extra_test.go b/client/client_extra_test.go index a0d8a72..55ae3e5 100644 --- a/client/client_extra_test.go +++ b/client/client_extra_test.go @@ -3,6 +3,7 @@ package client import ( "context" "errors" + "net/http" "strings" "testing" @@ -72,3 +73,51 @@ type errStore struct{} func (errStore) Get(context.Context) (string, error) { return "", errors.New("boom") } func (errStore) Set(context.Context, string) error { return nil } + +// SPEC: MS-ASHTTP/client.profile.force-http11 +func TestNew_ForceHTTP11_DefaultTransportDisablesHTTP2(t *testing.T) { + c, err := New(Config{ + BaseURL: "http://example.invalid/Microsoft-Server-ActiveSync", + DeviceID: "d", + DeviceType: "t", + ForceHTTP11: true, + }) + if err != nil { + t.Fatalf("New: %v", err) + } + tr, ok := c.HTTPClient.Transport.(*http.Transport) + if !ok { + t.Fatalf("Transport type %T", c.HTTPClient.Transport) + } + if tr.TLSNextProto == nil { + t.Fatal("TLSNextProto is nil, want non-nil empty map") + } + if len(tr.TLSNextProto) != 0 { + t.Fatalf("TLSNextProto len = %d, want 0", len(tr.TLSNextProto)) + } +} + +// SPEC: MS-ASHTTP/client.profile.force-http11 +func TestNew_ForceHTTP11_CustomHTTPClientUnchanged(t *testing.T) { + base := &http.Transport{} + hc := &http.Client{Transport: base} + c, err := New(Config{ + BaseURL: "http://example.invalid/Microsoft-Server-ActiveSync", + HTTPClient: hc, + DeviceID: "d", + DeviceType: "t", + ForceHTTP11: true, + }) + if err != nil { + t.Fatalf("New: %v", err) + } + if c.HTTPClient != hc { + t.Fatal("HTTPClient replaced") + } + if c.HTTPClient.Transport != base { + t.Fatal("Transport replaced") + } + if base.TLSNextProto != nil { + t.Fatalf("custom transport TLSNextProto = %v, want nil", base.TLSNextProto) + } +} diff --git a/client/client_profile_test.go b/client/client_profile_test.go new file mode 100644 index 0000000..ea01961 --- /dev/null +++ b/client/client_profile_test.go @@ -0,0 +1,142 @@ +package client + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/remdev/go-activesync/eas" + "github.com/remdev/go-activesync/wbxml" +) + +// SPEC: MS-ASHTTP/client.transport.force-http11 +func TestNew_ForceHTTP11_TLSNextProtoEmptyMap(t *testing.T) { + c, err := New(Config{ + BaseURL: "https://example.invalid/Microsoft-Server-ActiveSync", + DeviceID: "d", + DeviceType: "t", + ForceHTTP11: true, + }) + if err != nil { + t.Fatalf("New: %v", err) + } + tr, ok := c.HTTPClient.Transport.(*http.Transport) + if !ok { + t.Fatalf("Transport type got %T", c.HTTPClient.Transport) + } + if tr.TLSNextProto == nil { + t.Fatal("TLSNextProto is nil; expected non-nil empty map to disable HTTP/2 ALPN") + } + if len(tr.TLSNextProto) != 0 { + t.Fatalf("TLSNextProto len = %d; want 0", len(tr.TLSNextProto)) + } +} + +// SPEC: MS-ASHTTP/client.transport.force-http11 +func TestNew_ForceHTTP11_WithCustomHTTPClientIgnored(t *testing.T) { + custom := &http.Client{Transport: http.DefaultTransport} + c, err := New(Config{ + BaseURL: "http://example.invalid/Microsoft-Server-ActiveSync", + DeviceID: "d", + DeviceType: "t", + HTTPClient: custom, + ForceHTTP11: true, + }) + if err != nil { + t.Fatalf("New: %v", err) + } + if c.HTTPClient != custom { + t.Fatal("ForceHTTP11 must not replace a caller-supplied HTTPClient") + } +} + +// SPEC: MS-ASHTTP/client.extra-headers-merge +func TestProvision_OutboundExtraHeaders(t *testing.T) { + var saw string + calls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + saw = r.Header.Get("X-Integration-Probe") + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + var req eas.ProvisionRequest + if err := wbxml.Unmarshal(body, &req); err != nil { + http.Error(w, err.Error(), 400) + return + } + calls++ + var resp eas.ProvisionResponse + switch calls { + case 1: + resp = eas.ProvisionResponse{ + Status: int32(eas.StatusSuccess), + Policies: eas.PoliciesResponse{ + Policy: []eas.PolicyResponse{{ + PolicyType: eas.PolicyTypeWBXML, + PolicyKey: "temp-key", + Status: int32(eas.StatusSuccess), + Data: &eas.EASProvisionDoc{ + DevicePasswordEnabled: 1, + MinDevicePasswordLength: 4, + MaxInactivityTimeDeviceLock: 900, + MaxDevicePasswordFailedAttempts: 8, + AllowSimpleDevicePassword: 1, + AllowStorageCard: 1, + AllowCamera: 1, + RequireDeviceEncryption: 0, + AlphanumericDevicePasswordRequired: 0, + }, + }}, + }, + } + case 2: + resp = eas.ProvisionResponse{ + Status: int32(eas.StatusSuccess), + Policies: eas.PoliciesResponse{ + Policy: []eas.PolicyResponse{{ + PolicyType: eas.PolicyTypeWBXML, + PolicyKey: "final-key", + Status: int32(eas.StatusSuccess), + }}, + }, + } + default: + http.Error(w, "unexpected call", 500) + return + } + data, err := wbxml.Marshal(&resp) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", ContentTypeWBXML) + _, _ = w.Write(data) + })) + t.Cleanup(srv.Close) + + h := http.Header{} + h.Set("X-Integration-Probe", "present") + c, err := New(Config{ + BaseURL: srv.URL + EndpointPath, + HTTPClient: srv.Client(), + Auth: &BasicAuth{Username: "u", Password: "p"}, + DeviceID: "DEV", + DeviceType: "Outlook", + UserAgent: "ua-test/1", + Locale: 0x0419, + ExtraHeaders: h, + }) + if err != nil { + t.Fatalf("New: %v", err) + } + if _, err := c.Provision(context.Background(), "user@example.com"); err != nil { + t.Fatalf("Provision: %v", err) + } + if saw != "present" { + t.Fatalf("X-Integration-Probe = %q", saw) + } +} diff --git a/client/headers.go b/client/headers.go index 1fea6a8..8aff887 100644 --- a/client/headers.go +++ b/client/headers.go @@ -29,3 +29,30 @@ func ApplyMandatoryHeaders(h http.Header, opts HeaderOptions) { h.Set("Accept-Language", opts.AcceptLanguage) } } + +// mergeExtraHeaders merges src into dst for integrator-specific headers. Each +// header name is normalized with http.CanonicalHeaderKey. If dst already +// contains any value for that name, the entire key is skipped so mandatory +// client headers cannot be overwritten. Otherwise every value from src for +// that key is added with Add (preserving duplicates from src). +func mergeExtraHeaders(dst, src http.Header) { + if len(src) == 0 { + return + } + grouped := make(map[string][]string) + for k, vals := range src { + if k == "" { + continue + } + ck := http.CanonicalHeaderKey(k) + grouped[ck] = append(grouped[ck], vals...) + } + for ck, vals := range grouped { + if len(dst.Values(ck)) > 0 { + continue + } + for _, v := range vals { + dst.Add(ck, v) + } + } +} diff --git a/client/headers_test.go b/client/headers_test.go index 120c655..5308673 100644 --- a/client/headers_test.go +++ b/client/headers_test.go @@ -53,3 +53,54 @@ func TestApplyMandatoryHeaders_AcceptLanguage(t *testing.T) { t.Errorf("Accept-Language = %q", got) } } + +// SPEC: MS-ASHTTP/client.extra-headers-merge +func TestMergeExtraHeaders(t *testing.T) { + t.Run("addsAbsent", func(t *testing.T) { + dst := http.Header{} + src := http.Header{"X-Integrator": []string{"a", "b"}} + mergeExtraHeaders(dst, src) + got := dst.Values("X-Integrator") + if len(got) != 2 || got[0] != "a" || got[1] != "b" { + t.Fatalf("X-Integrator = %q", got) + } + }) + t.Run("skipsExistingKeys", func(t *testing.T) { + dst := http.Header{} + ApplyMandatoryHeaders(dst, HeaderOptions{ + ProtocolVersion: "14.1", + UserAgent: "official-ua", + }) + src := http.Header{ + "User-Agent": []string{"should-not-apply"}, + "Ms-Asprotocolversion": []string{"99.0"}, + "X-Integrator": []string{"ok"}, + } + mergeExtraHeaders(dst, src) + if dst.Get("User-Agent") != "official-ua" { + t.Errorf("User-Agent = %q", dst.Get("User-Agent")) + } + if dst.Get("MS-ASProtocolVersion") != "14.1" { + t.Errorf("MS-ASProtocolVersion = %q", dst.Get("MS-ASProtocolVersion")) + } + if dst.Get("X-Integrator") != "ok" { + t.Errorf("X-Integrator = %q", dst.Get("X-Integrator")) + } + }) + t.Run("canonicalizesKeys", func(t *testing.T) { + dst := http.Header{} + src := http.Header{"x-custom-device": []string{"model-9"}} + mergeExtraHeaders(dst, src) + if got := dst.Get("X-Custom-Device"); got != "model-9" { + t.Errorf("X-Custom-Device = %q", got) + } + }) + t.Run("duplicateValuesInSrc", func(t *testing.T) { + dst := http.Header{} + src := http.Header{"X-Two": []string{"a", "b"}} + mergeExtraHeaders(dst, src) + if dst.Values("X-Two") == nil || len(dst.Values("X-Two")) != 2 { + t.Fatalf("expected two values, got %#v", dst.Values("X-Two")) + } + }) +} diff --git a/client/integration_test.go b/client/integration_test.go index 50785c0..4d9f5d4 100644 --- a/client/integration_test.go +++ b/client/integration_test.go @@ -134,6 +134,46 @@ func TestProvision_TwoPhase(t *testing.T) { } } +// SPEC: MS-ASHTTP/client.profile.extra-headers +func TestFolderSync_OutgoingExtraHeaders(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req eas.FolderSyncRequest + decodeWBXML(t, r, &req) + if got := r.Header.Get("X-Device-Model"); got != "Surface" { + t.Errorf("X-Device-Model = %q", got) + } + if got := r.Header.Get("User-Agent"); got != "go-activesync-test/1.0" { + t.Errorf("User-Agent should stay mandatory client UA, got %q", got) + } + writeWBXML(t, w, &eas.FolderSyncResponse{ + Status: int32(eas.StatusSuccess), + SyncKey: "FS-X", + }) + })) + t.Cleanup(srv.Close) + + extra := http.Header{ + "x-device-model": []string{"Surface"}, + "User-Agent": []string{"should-not-override"}, + } + c, err := New(Config{ + BaseURL: srv.URL + EndpointPath, + HTTPClient: srv.Client(), + Auth: &BasicAuth{Username: "user@example.com", Password: "secret"}, + DeviceID: "TESTDEVICE", + DeviceType: "SmartPhone", + UserAgent: "go-activesync-test/1.0", + Locale: 0x0409, + ExtraHeaders: extra, + }) + if err != nil { + t.Fatalf("New: %v", err) + } + if _, err := c.FolderSync(context.Background(), "user@example.com", "0"); err != nil { + t.Fatalf("FolderSync: %v", err) + } +} + // SPEC: MS-ASCMD/scenario.full // SPEC: MS-ASCMD/foldersync.response func TestFolderSync_Initial(t *testing.T) { diff --git a/internal/spec/coverage.csv b/internal/spec/coverage.csv index 5d07903..a8e4a25 100644 --- a/internal/spec/coverage.csv +++ b/internal/spec/coverage.csv @@ -72,6 +72,8 @@ MS-ASHTTP/headers.accept-language,MS-ASHTTP,§2.2.2,Accept-Language header is ho MS-ASHTTP/auth.basic,MS-ASHTTP,§2.2.2,Authorization Basic header carries username:password base64-encoded,required MS-ASHTTP/store.policy,MS-ASHTTP,§2.2.2,PolicyStore Get/Set persist the current policy key across calls,required MS-ASHTTP/store.syncstate,MS-ASHTTP,§2.2.2,SyncStateStore Get/Set persist the current SyncKey per collection,required +MS-ASHTTP/client.extra-headers-merge,MS-ASHTTP,§2.2.2,Optional integrator HTTP headers are merged after mandatory headers without overwriting keys already present on the request,required +MS-ASHTTP/client.transport.force-http11,MS-ASHTTP,§2.2.1,When HTTPClient is nil and ForceHTTP11 is set the default Transport disables HTTP/2 ALPN via TLSNextProto for HTTPS interoperability,required MS-OXDISCO/request.schema,MS-OXDISCO,§3.1.5,POX request uses mobilesync requestschema 2006 and AcceptableResponseSchema is the matching responseschema,required MS-OXDISCO/request.path,MS-OXDISCO,§3.1.4,POST /autodiscover/autodiscover.xml as Content-Type text/xml UTF-8,required MS-OXDISCO/response.url,MS-OXDISCO,§3.1.5,Response Action/Settings/Server with Type MobileSync exposes the EAS endpoint URL,required @@ -106,3 +108,5 @@ MS-ASCMD/ping.status.changes,MS-ASCMD,§2.2.1.13.6,Ping Status=2 indicates one o MS-ASCMD/global.status.codes,MS-ASCMD,§2.2.4,Global Status codes include 142 invalid policy; 143 invalid policy key; 144 invalid device id,required MS-ASCMD/retry.142,MS-ASCMD,§2.2.4,Status 142 triggers automatic re-provision with a fresh PolicyKey before retrying the original command,required MS-ASCMD/scenario.full,MS-ASCMD,§3.1,End-to-end Provision -> FolderSync -> Sync -> Ping scenario over HTTP exchanges WBXML payloads with the negotiated PolicyKey and SyncKey,required +MS-ASHTTP/client.profile.extra-headers,MS-ASHTTP,§2.2.2,Config.ExtraHeaders are merged after mandatory MS-ASHTTP headers without overwriting any header key already present on the request,required +MS-ASHTTP/client.profile.force-http11,MS-ASHTTP,§2.2.1,When HTTPClient is nil and ForceHTTP11 is true New configures a client transport cloned from DefaultTransport with TLSNextProto set to a non-nil empty map to disable HTTP/2,required From d3c284b289adbf5e3a3979c80ba7f82bcbb6fc12 Mon Sep 17 00:00:00 2001 From: Misha <6481198+remdev@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:00:29 +0300 Subject: [PATCH 2/2] fix(client): pr feedback on api stability and force-http11 defaults Move ExtraHeaders and ForceHTTP11 after Policy/Sync stores so positional Config literals stay valid. Copy http.DefaultClient when swapping Transport for ForceHTTP11 (preserve Timeout and other defaults). Document header map mutability; note append-only field order in AGENTS.md. --- AGENTS.md | 3 +++ client/client.go | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f4350db..16c25a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,9 @@ sprinkling suppressions. - Module path: `github.com/remdev/go-activesync`. Do not introduce other paths in imports, examples, or docs. +- Append new exported fields at the **end** of configuration structs (for + example `client.Config`) so positional composite literals in downstream code + remain compatible across minor updates. - Commit author / committer is governed by the local git config. Do not rewrite history that has already been pushed to `origin`. diff --git a/client/client.go b/client/client.go index 0c0a09d..c2f3931 100644 --- a/client/client.go +++ b/client/client.go @@ -28,16 +28,17 @@ type Client struct { ProtocolVersion string AcceptLanguage string + PolicyStore PolicyStore + SyncStateStore SyncStateStore + // ExtraHeaders are merged into each request after mandatory headers without - // overwriting keys already set (see Config.ExtraHeaders). + // overwriting keys already set (see Config.ExtraHeaders). Do not mutate this + // map after New while the Client is in use; concurrent writes race with requests. ExtraHeaders http.Header // ForceHTTP11 reflects the config flag; when HTTPClient was supplied to New // the transport is never altered and this bit is informational only. ForceHTTP11 bool - - PolicyStore PolicyStore - SyncStateStore SyncStateStore } // Config bundles the values required to construct a Client. @@ -60,10 +61,16 @@ type Config struct { AcceptLanguage string + PolicyStore PolicyStore + SyncStateStore SyncStateStore + // ExtraHeaders optional integrator headers (device model, OS, or other // vendor expectations). They are merged after mandatory headers and never // replace keys the client already set; device model/OS are not separate // Config fields because MS-ASHTTP only standardizes the query DeviceType. + // + // Avoid mutating this header map after passing Config to New if other + // goroutines still hold a reference to it; New clones into the Client when non-empty. ExtraHeaders http.Header // ForceHTTP11, when true and HTTPClient is nil, builds an HTTP client whose @@ -71,9 +78,6 @@ type Config struct { // TLSNextProto to a non-nil empty map. When HTTPClient is non-nil, // ForceHTTP11 is ignored and the caller's transport is not modified. ForceHTTP11 bool - - PolicyStore PolicyStore - SyncStateStore SyncStateStore } // New returns a Client populated with sensible defaults for any unset @@ -97,9 +101,9 @@ func New(cfg Config) (*Client, error) { Locale: cfg.Locale, ProtocolVersion: eas.ProtocolVersion, AcceptLanguage: cfg.AcceptLanguage, - ForceHTTP11: cfg.ForceHTTP11, PolicyStore: cfg.PolicyStore, SyncStateStore: cfg.SyncStateStore, + ForceHTTP11: cfg.ForceHTTP11, } if len(cfg.ExtraHeaders) > 0 { c.ExtraHeaders = cfg.ExtraHeaders.Clone() @@ -115,7 +119,9 @@ func New(cfg Config) (*Client, error) { } tr := dt.Clone() tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) - c.HTTPClient = &http.Client{Transport: tr} + hc := *http.DefaultClient + hc.Transport = tr + c.HTTPClient = &hc default: c.HTTPClient = http.DefaultClient }