opensearch-go v4.7.0
CRITICAL UPGRADE RELEASE NOTE - The next major release (v5) has substantially expanded error handling capabilities compared to v4.
v4.7.0 is the last v4 release before v5. The v4 -> v5 upgrade changes runtime error-handling behavior and is NOT a drop-in replacement across major versions. If you use this client, read this now - even if you are only upgrading to v4.7.0.
Today, v4 only returns transport-level errors; partial failures (e.g. failed bulk items, failed shards, unconfirmed replica writes) are reported inside the response body, not as Go
errorvalues. In v5, those partial failures are returned as errors by default. Code that compiles and passes against v4 can behave differently against v5 without any code change.Upgrading between v4 releases (any
<v4.7.0to anyv4.X.X) does not change the default error-handling semantics. The change lands in v5, which is expected shortly after this release.This is why v4.7.0 exists: it lets you adopt the v5 error behavior on v4 today - via one environment variable, no code change - so you can validate your error handling before v5 ships and upgrade with no surprises. See Error handling below.
This release covers development from December 2025 through July 2026 (v4.6.0 -> v4.7.0). Three themes dominate the release: a reworked error-handling model that surfaces partial failures as typed Go errors, a rewritten transport layer, and a client-side routing layer that replaces plain round-robin node selection. It also ships a preview of the v5 API surface.
Several of these features are disabled by default in v4 and enabled by default in v5. Each can be turned on in v4 through an environment variable, so users can evaluate the v5 behavior against their own clusters with no code changes before upgrading.
Full Changelog: v4.6.0...v4.7.0
1. Error handling
Background
OpenSearch returns HTTP 200 for many operations that only partially succeed: bulk requests where some items fail, searches where some shards error, and writes where a replica fails to confirm. A 2xx status code does not mean the whole operation succeeded.
Before v4.7.0, only transport errors were returned as errors and any partial or shard-level failure required inspecting response fields by hand after every call. v4.7.0 adds a model that turns partial failures into typed Go errors:
| Error type | Returned by |
|---|---|
*PartialBulkError |
Bulk |
*PartialSearchError |
Search, MSearch, SearchTemplate, Scroll.Get |
*ShardFailureError |
Index, Document.Create, Document.Delete, Update |
*MultiSearchItemError |
MSearch, MSearchTemplate (per sub-response) |
Which categories are returned as errors is controlled by a per-category mask on Config.Errors. When a category is masked, the operation returns its response with a nil error even though the response body records failures, and the caller is responsible for inspecting it. When a category is not masked (the default coming in v5), the same partial failure is returned as one of the typed errors above, and the response is still fully populated alongside the error.
v4 -> v5 Migration Path
The default mask in v4 is to mask all errors to preserve the existing v4 behavior of only returning transport errors. The v4.7.0 release exists to catch this change in behavior between v4 and v5 of the library in a forward-compatible way.
| Surface | Config.Errors == nil means |
Effect |
|---|---|---|
| v4 | errmask.All |
every category masked: partial failures are not returned as errors (preserves pre-4.7 behavior) |
| v5+ | errmask.Empty |
no category masked: every partial failure is returned as an error |
In other words: a v4 program that never sets Config.Errors sees the same silent behavior it always has. The identical program compiled against v5 will begin receiving partial failures as error values. This release lets you adopt the v5 behavior on v4 ahead of time so the upgrade holds no surprises.
Transitioning to v5 error handling
Through the use of environment variables, callers can change the runtime behavior of v4 code to test and migrate to v5's error handling semantics.
# Report every partial-failure category as an error (the v5 default)
export OPENSEARCH_GO_ERROR_MASK=empty
# Report everything except bulk-item failures
export OPENSEARCH_GO_ERROR_MASK="+all,-bulk_items"
# Mask everything (the v4 default, stated explicitly)
export OPENSEARCH_GO_ERROR_MASK=allError masks are comma-separated, lowercase, snake_case category names (e.g. bulk_items, search_shards, write_shards, multi_search_items) with +/- prefixes (default mask operator is + if omitted). The special tokens all (mask every category) and empty (mask none) set the whole mask at once; category tokens adjust individual bits from there. Unknown tokens are ignored for forward compatibility.
In code:
// Adopt the v5 default (report every category) on v4:
mask := errmask.Empty
client, err := opensearchapi.NewClient(opensearchapi.Config{
Client: opensearch.Config{Addresses: []string{"https://localhost:9200"}},
Errors: &mask,
})In v4, the default error mask is errmask.New(errmask.All), which masks everything (i.e. preserves the existing v4 behavior). Custom error masks, such as errmask.New(errmask.SearchShards | errmask.MultiSearchItems) masks specific categories. In v5 the default error mask changes to errmask.New(errmask.Empty).
Idiomatic OpenSearch Error Handling
The recommended call-site pattern is a for/switch over opensearchapi.Errors(err), which flattens single- and multi-wrapper error shapes into a uniform slice:
resp, err := client.Bulk(ctx, opensearchapi.BulkReq{Body: body})
for _, sub := range opensearchapi.Errors(err) {
switch e := sub.(type) {
case *opensearchapi.PartialBulkError:
// resp is fully populated -- inspect individual items
log.Printf("%d/%d items failed",
len(e.FailedItems),
e.SucceededCount+len(e.FailedItems))
for _, item := range e.FailedItems {
log.Printf(" %s %s/%s: %s",
item.Error.Type, item.Index, item.ID, item.Error.Reason)
}
default:
return err // transport or HTTP error
}
}Helpers IsPartialFailure, ToleratePartialFailures, and RequireSuccessRate support threshold-based tolerance.
See guides/error_handling.md for v4 and v5preview examples side by side, the full category reference, and the rationale for the type switch over errors.As.
2. Rewritten transport
The transport layer was reworked for thread safety, correctness, and lower per-request overhead.
Thread-safety and deadlock fixes
The transport migrated to a struct-embedded mutex pattern with atomic counters for hot-path state (#779, fixing #775). This work resolved three deadlocks:
- Connection resurrection deadlock -
scheduleResurrectre-acquired a lock it already held; fixed by passingdeadSinceas a parameter so the value is read once before the lock is released. - Connection-pool test deadlocks -
connection_internal_test.goreleased and re-acquired locks in an order that could deadlock under-race; fixed by extracting state before releasing the lock. - Bulk-indexer
Closedeadlock - the implicit bulk-indexer client could deadlock onClosewhen the flusher had already exited; the flusher now stops via context cancellation instead of a done channel, soCloseno longer races it (#932).
Correctness fixes
- Bulk-indexer data corruption (present since 2021):
_idandroutingvalues containing<,>, or&were HTML-escaped before transmission, so OpenSearch stored the escaped form. This caused duplicate documents, unreachable data on read-by-ID, and shard-routing mismatches. Fixed by disabling HTML escaping in the bulk meta encoder. #824 - Goroutine leaks and permanently-dead connections - pool replacement during node discovery orphaned resurrection goroutines, leaving connections dead with no health checker; multi-to-single pool demotion leaked goroutines. Both are resolved with proper context cancellation. #786, #830
- Seed fallback masked by unreachable discovered nodes - in NAT'd or Kubernetes clusters, discovery could insert unverified, unroutable nodes that took the request stream and prevented the seed-URL fallback from firing. A node is now routable only once confirmed reachable. #952, #956
BulkIndexerStats.NumAddedovercounting items rejected on context cancellation; adds aBulkAddFailCountcounter. #783- gzip buffer-pool nil poisoning on compress error, dropped response-body read errors, and several response-body lifecycle fixes that had defeated HTTP keep-alive. #859
- Double-slash URL paths across 74
GetRequestmethods, where an empty segment produced//thathttp.NewRequestmisparsed as an authority separator, replaced with typed path builders that reject empty required segments. #804 - Compatibility fixes for OpenSearch 3.1 through 3.6. CI now tests against OpenSearch 2.19.x and 3.6.0.
New capabilities
opensearch.Do[T]()enforces pointer response types at compile time, preventing a class of bugs where a non-pointer value failed to decode at runtime. Anopensearch.NoBodymarker covers calls that return no body.Client.Do()remains available;staticchecknow directs callers towardDo[T].Client.Close()onopensearch.Clientandopensearchapi.Clientreleases node-discovery, health/stats polling, and DNS-refresh goroutines along with idle connections.opensearchutil.NewBulkIndexercloses the client it creates implicitly.Stream()provides raw byte forwarding for proxy and streaming use cases.RequestTimeoutbounds each attempt to prevent hangs on stalled connections.- Lock-free metrics - per-request counters and connection dead/overloaded timestamps moved to atomics, removing the primary metrics-snapshot lock contention. Per-request counters are now collected regardless of
EnableMetrics. InsecureSkipVerifyconfig option disables TLS verification without a customhttp.Transport, retaining connection pooling, HTTP/2, and timeout defaults.- Reduced per-request allocations - direct
*http.Requestconstruction andsync.Pool-backed buffers reduce typical operations from 8 allocations / 2930 B to 2 allocations / 472 B.
The routing env var OPENSEARCH_GO_ROUTER=true also enables node auto-discovery, described in the next section.
3. Routing (versus round-robin)
The default client behavior is to use a round-robin node selector. Starting in v4.7.0, the client has a new optional routing layer that accounts for cluster topology and load when choosing a node for each request. It is disabled by default in v4 and enabled by default in v5.
Enabling Routing in v4
# Enable the default router and node auto-discovery
export OPENSEARCH_GO_ROUTER=trueOr pass a Router in Config. OPENSEARCH_GO_ROUTER=false opts out (relevant when testing against v5preview, where it is on by default).
Required permissions (Security plugin)
On a secured cluster, the router's discovery and health probes need read-only monitoring privileges: cluster:monitor/nodes (node discovery via /_nodes/...) and cluster:monitor/health (health probe via /_cluster/health). If a service account lacks them, the client degrades gracefully - it falls back to GET / for health and keeps using seed URLs - so routing may silently under-perform rather than error. If routing isn't behaving as expected on a locked-down cluster, check these first. Least-privilege role setup is in guides/cluster_health_checking.md and guides/node_discovery_and_roles.md.
Benefits over round-robin
- Role-aware routing - bulk, reindex, and streaming operations route to ingest nodes; searches, mget, scroll, PIT, and field-caps route to search/data nodes; writes and shard-maintenance operations route to data nodes. Dedicated cluster managers are excluded from request routing by default, matching the Java client.
- Shard-aware targeting - client-side murmur3 hashing mirrors OpenSearch's shard routing, so
?routing=and document-ID requests reach a node hosting the target shard, improving cache locality. Falls back to rendezvous hashing when shard maps are unavailable. - Congestion-aware scoring - per-pool AIMD congestion windows, RTT bucketing that prefers AZ-local nodes and overflows to remote AZs under load, and thread-pool stats polling. Overloaded nodes (heap above 85%, tripped breakers, HTTP 429) are demoted and recovered automatically, rather than continuing to receive an even share of traffic.
- Adaptive
max_concurrent_shard_requests- derived from a cluster-wide congestion signal, clamped to a[floor, cap]range, and never applied over an explicit caller value. - Seed-URL fallback - when all router pools are exhausted, requests fall back to the original seed URLs and trigger rediscovery. Disable with
OPENSEARCH_GO_FALLBACK=false. - Observability -
OnRouteevents with scoring detail,RouterSnapshotinClient.Metrics(), and per-connection RTT and load inspection.
Configuration
Finer control is available through RouterOption values (WithMinFanOut, WithMaxFanOut, WithShardCosts, WithAdaptiveConcurrency, WithShardExactRouting, and others) and environment variables (OPENSEARCH_GO_ROUTING_CONFIG, OPENSEARCH_GO_DISCOVERY_CONFIG, OPENSEARCH_GO_SHARD_COST, OPENSEARCH_GO_SHARD_REQUESTS, OPENSEARCH_GO_POLICY_*).
Guides
guides/routing.md- routing architecture, connection scoring, pool lifecycle, cost model, and the full environment-variable reference.guides/node_discovery_and_roles.md- node discovery and role-based selection.guides/metrics.md- client-side metrics and connection/policy/router snapshots.
v5 preview
The v5preview/opensearchapi package provides the v5 API surface within the v4 module, enabling incremental migration:
import "github.com/opensearch-project/opensearch-go/v4/v5preview/opensearchapi"It is a typed client generated by cmd/osgen from the OpenSearch API specification: consistent Req / Resp / Params triples, sub-clients mirroring OpenSearch namespaces (client.Cat, client.Cluster, client.Indices, and others), and a plugins/ subtree for ML, k-NN, security, and ISM. It coexists with the v4 opensearchapi/ package during the transition.
In v5preview, defaults match what v5 will ship (estimated release: mid-to-late July 2026):
- Router injected by default (v4: off);
OPENSEARCH_GO_ROUTER=falseopts out. - Partial failures reported by default (v4: masked).
Error handling in v5preview
The error model ports directly from v4 -- the same opensearchapi.Errors(err) slice and the same typed errors (*PartialBulkError, etc.) -- so the v4 handling pattern transfers unchanged in shape. The one difference is that the spec-generated response types make several fields pointers (BulkRespItem.ID, BulkRespItem.Error, ErrorCause.Reason), so nil-check before dereferencing. Compare this to the v4 example above:
resp, err := client.Bulk(ctx, opensearchapi.BulkReq{Body: body})
for _, sub := range opensearchapi.Errors(err) {
switch e := sub.(type) {
case *opensearchapi.PartialBulkError:
log.Printf("%d/%d items failed",
len(e.FailedItems),
e.SucceededCount+len(e.FailedItems))
for _, item := range e.FailedItems {
// ID, Error, and Reason are pointers in v5preview -- nil-check before use.
id := ""
if item.ID != nil {
id = *item.ID
}
if item.Error != nil {
reason := ""
if item.Error.Reason != nil {
reason = *item.Error.Reason
}
log.Printf(" %s %s/%s: %s",
item.Error.Type, item.Index, id, reason)
}
}
default:
return err
}
}Because v5preview reports partial failures by default, this loop fires without setting Config.Errors or OPENSEARCH_GO_ERROR_MASK -- the same code on v4 requires opting in first.
Further reading:
v5preview/opensearchapi/README.md- usage guidev5preview/opensearchapi/MIGRATING.md- v4 -> v5preview surface deltaUPGRADING.md- the>= 5.0.0section documents each default change
Generated API types in v5
v4.7.0 is the final release built on hand-written opensearchapi request/response structs. From v5, all request and response objects are generated from the OpenSearch API specification. This keeps the client aligned with the server API and removes hand-maintenance drift, and is why the spec-driven types are already present under v5preview.
Breaking changes in v4.7.0
These changes land in v4.7.0. Most surface at compile time:
opensearch.Requestinterface:GetRequest()becomesGetRequest(method string). Affects only code that implements or calls it directly.signer/awsmigrated from AWS SDK v1 to v2 (v1 reached end-of-support on July 31, 2025). The constructor now takesaws.Config.signer/awsv2remains available for transition. SeeUSER_GUIDE.md.- Dedicated cluster-manager nodes are excluded from request routing by default.
DiscoverNodes()and theDiscoverableinterface now take acontext.Context. CatTemplatesReq.TemplatesandIndexTemplateGetReq.IndexTemplateschange from[]stringtostring(a single name pattern; usestrings.Join(..., ",")for the prior multi-pattern behavior).- Typed failure arrays (
[]BulkByScrollFailure) replace[]json.RawMessagein by-query and reindex responses. Inline_shardsstructs are replaced withResponseShards.
Full details and migration snippets are in UPGRADING.md (>= 4.7.0 and >= 5.0.0 sections).
Upgrade guides and documentation
UPGRADING.md- version-by-version migration notesguides/error_handling.md- partial-failure errors, v4 and v5guides/routing.md- request routing and environment-variable referenceguides/node_discovery_and_roles.md- node discovery and rolesguides/cluster_health_checking.md- health-check capability detection and thecluster:monitorpermissions the router needs on a secured clusterguides/metrics.md- client-side metrics and connection/policy/router snapshotsUSER_GUIDE.md- general usage and AWS signer migration
Contributors
Thanks to everyone who contributed to this release: @sean-, @ryanyuan, @Jakob3xD, @ssunno, @shreyah963, @ramiyengar, @jeffmclean-cs, @evgenigourvitch, @duc2h, @Divyaasm, and @rmoles-cs.
New Contributors
- @ramiyengar (#825)
- @shreyah963 (#836)
- @ssunno (#834)
- @Divyaasm (#837)
- @evgenigourvitch (#784)
- @duc2h (#785)