go-unifi

Breaking changes

The authoritative 2.0.0 changelog — every public-API behavior and signature change, its impact and provenance, plus what's deferred to 3.0.0.

This is the authoritative changelog of every public-API behavior or signature change introduced in the 2.0.0 migration (epic #117). It is keyed to 10 breaking changes plus an additive Official surface. Each entry carries a verified status, a migration note, and a provenance link.

For a task-oriented, junior-to-senior walkthrough with rationale and grep hints for each change, see the 1.x upgrade guide.

Status key. DONE = the change is in the released 2.0.0 tree and verified against the actual code. PENDING = planned; not yet landed (see the Planned for 3.0.0 section below).

Landed — 10 breaking changes + additive Official surface

#ChangeImpactStatus
0Module path bumped to /v2HighDONE
1API-key authentication onlyHighDONE
2TLS verify-by-defaultHighDONE
3Go version bump to 1.26MediumDONE
4Client gains SetSettingMediumDONE
5Client gains *Context variantsMediumDONE
6v1 Create/Update no longer return ErrNotFoundMediumDONE
7meta.rc=="error" on HTTP 200 → *ServerErrorMediumDONE
8Map 404 → ErrNotFoundLowDONE
9UseLocking is a no-opLowDONE
10Remove CSRF handlingLowDONE
Official API surface (additive)AdditiveDONE

0. Module path bumped to /v2

Status: DONEgo.mod declares module github.com/filipowm/go-unifi/v2.

Import-path change (compile break). All import paths change from github.com/filipowm/go-unifi/unifi to github.com/filipowm/go-unifi/v2/unifi. Update go.mod with go get github.com/filipowm/go-unifi/v2 and run a global find-and-replace on your import statements.

// before
import "github.com/filipowm/go-unifi/unifi"
// after
import "github.com/filipowm/go-unifi/v2/unifi"

Migration: go get github.com/filipowm/go-unifi/v2 then find . -name '*.go' | xargs sed -i 's|filipowm/go-unifi/unifi|filipowm/go-unifi/v2/unifi|g'.

Provenance: go.mod, module major-version bump convention.

1. API-key authentication only

Status: DONE — landed in issue #125 (API-key-only auth).

Behavioral change + signature change (compile break). Username/password authentication (ClientConfig.User/Password/RememberMe) is retired. Every consumer must authenticate with an API key via ClientConfig.APIKey. The Login/LoginContext/Logout/LogoutContext methods are removed from the Client interface. Old-style (classic) controllers, which only supported user/pass auth, are no longer reachable and construction fails immediately with ErrOldStyleUnsupported.

Compile breaks: ClientConfig.User, .Password, .RememberMe removed; UserPassCredentials type removed; Login/LoginContext/Logout/LogoutContext methods removed from the Client interface; CsrfHeader constant removed; APIPaths.LoginPath/.LogoutPath fields removed.

Provenance: epic #117, issue #125 (API-key auth retirement).

2. TLS verify-by-default

Status: DONE — landed in Wave 1 (ARCH-06).

Signature change + behavioral flip (compile break + silent runtime change). ClientConfig.VerifySSL bool was renamed and inverted to SkipVerifySSL bool. The zero value now enables verification (secure by default).

// before
config := &unifi.ClientConfig{VerifySSL: false} // disabled verification
// after
config := &unifi.ClientConfig{SkipVerifySSL: true} // disable for self-signed cert

A caller that left VerifySSL unset got verification OFF; the same zero value now gives verification ON. Connections to self-signed controllers break silently at runtime — except the rename forces a compile error at every call site that touched the field, surfacing the flip at build time. Disabling verification is logged at WARN level on every client build.

Provenance: ARCH-06, Wave 1.

3. Go version bump to 1.26

Status: DONEgo 1.26.0 in go.mod.

Build constraint change. The module requires Go 1.26 or later. Consumers still on Go 1.24/1.25 must upgrade their toolchain.

Provenance: epic #117, go.mod.

4. Client gains SetSetting

Status: DONE — landed in Wave 1 (ARCH-08).

Interface addition (compile break for custom Client implementations). The Client interface declares:

SetSetting(ctx context.Context, site string, key string, reqBody any) (any, error)

The concrete client already implemented it; it was simply missing from the interface. Any third-party type that implements unifi.Client must add this method; the moq ClientMock is regenerated automatically.

Provenance: ARCH-08, Wave 1.

5. Client gains *Context variants

Status: DONE — landed in Wave 2 (TEST-15); Login/Logout ctx variants superseded by #125.

Interface addition (compile break for custom Client implementations). Two context-first lifecycle methods are part of the Client interface:

VersionContext(ctx context.Context) (string, error)
GetSystemInformationContext(ctx context.Context) (*SysInfo, error)

LoginContext/LogoutContext were added in Wave 2 but removed in issue #125 (API-key-only auth, row 1). The no-ctx Version/GetSystemInformation remain source-compatible. Any third-party type implementing unifi.Client must add these two methods; the moq ClientMock is regenerated automatically.

Provenance: TEST-15, Wave 2; #125 for Login/Logout removal.

6. v1 Create/Update no longer return ErrNotFound

Status: DONE — landed in Wave 2 (ARCH-13).

Behavioral change (no compile break). The v1-REST template used to return ErrNotFound from a successful create/update when the response data array length was not exactly 1 — semantically wrong and inconsistent with the v2 template. The ~29 generated v1 create<X>/update<X> methods now return a descriptive error instead:

fmt.Errorf("unexpected response: expected 1 <X>, got %d", n)

Any caller doing errors.Is(err, ErrNotFound) on a create or update path will no longer match — that branch was always incorrect. Treat any non-nil error from Create<X>/Update<X> as a failure. ErrNotFound remains the contract for Get<X> / single-resource lookups only.

Provenance: ARCH-13, Wave 2.

7. meta.rc=="error" on HTTP 200 → *ServerError

Status: DONE — landed in Wave 2 (ARCH-10).

Behavioral change (no compile break). The UniFi v1 API can return HTTP 200 with meta.rc=="error" (a soft / application-level failure). Previously this was caught in exactly one place (CreateUser) and silently swallowed everywhere else. The check is now centralized in handleResponse, so every decoded {meta,data} 200 with rc=="error" surfaces a *ServerError carrying the controller's rc/msg (enriched with status/method/URL).

var serverErr *unifi.ServerError
if errors.As(err, &serverErr) { /* ... */ }

It is not ErrNotFound (errors.Is(err, ErrNotFound) == false), so genuine empty-data 200s and real 404s are unaffected. CreateUser retains its nested per-object meta check.

Requests that pass nil as the response body (all generated deletes and fire-and-forget cmd/ operations) do not perform this check — the response body is not inspected when no response struct is expected.

Provenance: ARCH-10, Wave 2.

8. Map 404 → ErrNotFound

Status: DONE — landed in Wave 1 (ARCH-05).

Behavioral widening (no compile break). (*ServerError).Is maps a *ServerError with StatusCode == 404 to the ErrNotFound sentinel. Previously only the hand-written list/get wrappers returned ErrNotFound; now a genuine 404 from any endpoint also satisfies errors.Is(err, ErrNotFound). A consumer that distinguished "404 server error" from the ErrNotFound sentinel now sees them as equal.

Provenance: ARCH-05, Wave 1.

9. UseLocking is a no-op

Status: DONE — landed in Wave 1 (ARCH-04).

Behavioral change (no compile break). net/http.Client is goroutine-safe and the client no longer serializes requests. ClientConfig.UseLocking is retained for source compatibility but is marked // Deprecated: and has no effect — setting it true or false changes nothing. This no-op is not new in 2.0.0: UseLocking has been deprecated and inert since 1.11.0, so 1.11.x callers already saw no serialisation. Remove it from your config.

Provenance: ARCH-04, Wave 1.

10. Remove CSRF handling

Status: DONE — landed in issue #125 (API-key-only auth).

Type removal (compile break for direct users). With username/password auth retired (row 1), the CSRFInterceptor and its token-management logic are removed. The CsrfHeader constant is also removed.

Migration: remove any direct use of CSRFInterceptor or CsrfHeader; neither is relevant with API-key authentication.

Provenance: epic #117, issue #125 (API-key-only auth).

Official API surface (additive)

Status: DONE — landed in PR #119.

The Client interface gains Internal() and Official() accessors. Internal() returns the full InternalClient (all resource CRUD, identical to calling methods directly on the client). Official() returns the fluent Official OpenAPI client (integration/v1), gated behind ErrOfficialAPIUnavailable on unsupported controllers.

This is additive in 2.0.0 — existing code using resource methods directly on the client is unaffected. The default is expected to flip to Official() in 3.0.0. See entries D and E in the provenance index below for details.

Additional changes — provenance index

Changes that complement the 10-row table above. Nothing here is new; entries are relocated from the prior wave-by-wave structure for traceability.

A. CSRFInterceptor.CSRFToken: exported field → accessor method — superseded by #125

Superseded by row 10. CSRFInterceptor is now entirely removed. This Wave 1 intermediate step (field → accessor) is moot; the type is gone. Any code referencing CSRFInterceptor in any form fails to compile.

B. DpiApp/DpiGroup types removed (ARCH-08, Wave 1)

Type removal. The unused DpiApp and DpiGroup types and their CRUD were dead code — excluded from the Client interface yet still shipped — and are excluded from generation entirely. No Client method ever exposed them, so typical consumers are unaffected; any code directly referencing unifi.DpiApp / unifi.DpiGroup struct types must remove it. DPI settings remain available via SettingDpi.

C. AddInterceptor signature: *ClientInterceptorClientInterceptor (ARCH-18, Wave 2)

Signature change (compile break for direct callers). AddInterceptor now takes the interceptor by value, matching how the interceptor slice and ClientConfig.Interceptors are typed:

// before
c.AddInterceptor(&myInterceptor)
// after
c.AddInterceptor(myInterceptor)

Dedup semantics also changed: previously by interface == (could panic on non-comparable types); now by concrete type via reflect.TypeOf — only one interceptor of a given concrete type is retained, and non-comparable interceptors no longer panic. AddInterceptor is not part of the public Client interface, so consumers using the Client interface are unaffected.

D. Client interface split into InternalClient + Internal()/Official() accessors (#119)

The InternalClient embedded interface and the Internal()/Official() accessors implement the 2.0.0-canonical-Internal step: in 2.0.0 the embedded Internal surface stays the default, so existing code is untouched; 3.0.0 is expected to flip the default to the Official client (the 3.0.0-flip).

E. Official API unavailable on classic/old-style controllers (#119)

New runtime contract (no compile break). client.Official() always returns a non-nil client, but every operation is gated. They return:

  • ErrOfficialAPIDisabled when ClientConfig.DisableOfficialAPI is set (fails fast, no probe).
  • ErrOfficialAPIUnavailable on an old-style (classic) controller, a failed GET /v1/info probe (a rejected API key surfaces here), or a controller version below 10.1.78.

Match with errors.Is(err, unifi.ErrOfficialAPIUnavailable) / errors.Is(err, unifi.ErrOfficialAPIDisabled). The Internal API is unaffected.

F. Internal codegen-tool API changes (not public unifi surface)

These affect only forks of the generator:

  • DownloadAndExtract gained a leading *http.Client parameter (TEST-07, Wave 1).
  • DownloadAndExtract/downloadJar gained a leading context.Context parameter (ARCH-15, Wave 2).

G. NewBareClient replaced by NewClient + SkipSystemInfo

Status: DONE — landed; NewBareClient removed.

Signature change (compile break). The NewBareClient constructor is removed. Use NewClient with SkipSystemInfo: true to achieve the same behaviour (skip the eager GetSystemInformation() call).

// before
c, err := unifi.NewBareClient(&unifi.ClientConfig{URL: "...", APIKey: "..."})
// after
c, err := unifi.NewClient(&unifi.ClientConfig{URL: "...", APIKey: "...", SkipSystemInfo: true})

Provenance: unifi/client.go; delegated from epic #117, issue #148.

H. New Patch method on Client

Status: DONE — landed in unifi/requests.go.

Additive change (no compile break). The Client interface and concrete client expose a Patch method for HTTP PATCH requests, alongside the existing Do/Get/Post/Put/Delete.

// new in 2.0.0 — target endpoint must accept PATCH (e.g. the Official integration/v1 surface)
err := c.Patch(ctx, "/proxy/network/integration/v1/sites/<id>/...", patchBody, &resp)

Patch sends a literal HTTP PATCH; most legacy Internal REST resources (e.g. networkconf) expect PUT.

Provenance: unifi/requests.go; delegated from epic #117, issue #148.

I. Logger decoupled from the Client interface

Status: DONE — landed in issue #167.

Interface change (compile break for custom Client implementations and mocks). The Client interface no longer embeds Logger; it now exposes a single accessor Logger() Logger. The 10 logging methods (Trace/Debug/Info/Warn/Error + their *f variants) are therefore no longer promoted onto a Client value — call them through the accessor.

// before
c.Errorf("boom: %v", err)
// after
c.Logger().Errorf("boom: %v", err)

Any third-party type implementing unifi.Client (or a hand-rolled mock) previously had to satisfy all 10 logging methods; now it must implement only Logger() Logger. The moq ClientMock is regenerated automatically and exposes a single LoggerFunc/Logger() pair instead of the 10 logging hooks.

The Logger interface itself and the LoggingLevel enum are unchanged. The default logger is now backed by log/slog (no third-party logging dependency in the unifi package, no forced ANSI colours); NewSlogLogger(*slog.Logger) Logger is added to wrap a caller-supplied *slog.Logger. These are additive — only the Client-interface decoupling is a compile break.

Provenance: unifi/client.go, unifi/logging.go, codegen/internal/client.go.tmpl; issue #167.

Planned for 3.0.0

The changes below are PENDING — they are not in 2.0.0 and are deferred to the next major version. They keep their original epic-row numbers under a P (planned) prefix so they don't collide with the landed rows 4–6 above.

P4. OpenAPI-shaped structs (deferred to 3.0.0)

Status: PENDING — no resources migrated yet; landing per-resource as part of the OpenAPI generator wave.

Type change (compile break for each migrated resource). Resources generated from the official OpenAPI spec (integration.json) may adopt different field names, types, or nesting than the legacy reverse-engineered shapes. Each resource is migrated individually; the breaking surface is bounded to that resource's struct. Migration is compile-error-driven: build against the new module version and fix any field references reported by the compiler.

Provenance: epic #117, OpenAPI-generator wave (issues TBD).

P5. Occasional field renames (deferred to 3.0.0)

Status: PENDING — no field renames yet; land alongside each OpenAPI-driven resource migration.

Signature change (compile break per renamed field). Where the official spec uses a different field name from the legacy one, the generated struct adopts the spec name. Each rename is documented here when it lands; rename at each call site.

Provenance: epic #117, per-resource OpenAPI migration.

P6. New integration/v1 APIStyle (deferred to 3.0.0)

Status: PENDING — routing generated resources through the official integration/v1 API is part of the OpenAPI generator wave (same milestone as P4/P5); not yet landed.

Interface change (compile break for custom Client implementations). A new APIStyle constant will route code-generated resources through the UniFi official integration/v1 API path instead of the legacy /api/s/{site}/... endpoints. The generated CRUD methods and their structs will adopt OpenAPI-derived shapes (see P4/P5).

The integration/v1 path is a capability layered on the new-style API — it is not a fourth independent style alongside V1, V2, and new-style; see unifi/api_paths.go for the documented constraint.

The Official() accessor and the integration/v1 info/sites vertical that did land in PR #119 are documented in entries D and E of the provenance index above.

Provenance: epic #117, OpenAPI generator wave (issues TBD, same milestone as P4/P5).

On this page