Skip to content

Add SPOG (Custom URL) routing support via x-databricks-org-id header#347

Merged
msrathore-db merged 8 commits intomainfrom
spog-fixes
Apr 21, 2026
Merged

Add SPOG (Custom URL) routing support via x-databricks-org-id header#347
msrathore-db merged 8 commits intomainfrom
spog-fixes

Conversation

@msrathore-db
Copy link
Copy Markdown
Contributor

@msrathore-db msrathore-db commented Apr 13, 2026

Summary

On SPOG (Custom URL / account-level) workspaces, httpPath has the form /sql/1.0/warehouses/<id>?o=<workspaceId>. The ?o= parameter routes Thrift calls correctly via the URL, but other endpoints (telemetry push, feature-flag check) run on separate hosts and need x-databricks-org-id as an HTTP header to route to the right workspace. Without it, those requests 404 or get misrouted on SPOG hosts.

Change

All contained in connector.go:

  1. extractSpogHeaders(httpPath string) map[string]string — parses the ?o= query param using url.ParseQuery (stdlib, not regex). Returns {"x-databricks-org-id": "<workspaceId>"} or nil. Three DEBUG log paths cover: malformed query, missing ?o=, and successful extraction.

  2. headerInjectingTransport — a lightweight http.RoundTripper wrapper that clones the request per the contract and sets the provided headers if not already set by the caller.

  3. withSpogHeaders(base *http.Client, headers map[string]string) *http.Client — returns a new client with the same settings but a wrapped transport.

  4. In Connect(), when extractSpogHeaders returns non-nil, the driver passes a wrapped client into TelemetryInitOptions.HTTPClient. The wrapped client is used for both the feature-flag check and the telemetry push. The driver's own c.client is left alone, so Thrift routing (which uses ?o= in the URL) is unaffected.

Why a transport wrapper instead of threading a parameter

An earlier revision of this PR threaded an extraHeaders parameter through telemetry.TelemetryInitOptionsisTelemetryEnabledfeatureFlagCache.isTelemetryEnabledfetchFeatureFlag. That approach:

  • Required API-surface changes in 3 telemetry files (config.go, featureflag.go, driver_integration.go).
  • Only covered the feature-flag check; telemetry/exporter.go (telemetry push) still sent Content-Type as the only header — SPOG routing would 404 at push time.

The RoundTripper wrapper:

  • Keeps telemetry/* identical to origin/main. Zero API churn.
  • Automatically applies to every outbound request using the wrapped client — feature-flag check, telemetry push, and any future HTTP paths that reuse it.
  • Respects caller-set headers (if a request already has x-databricks-org-id for some reason, the wrapper does not override).

Endpoints covered

Endpoint Uses wrapped client? Gets x-databricks-org-id?
Feature flags /api/2.0/connector-service/feature-flags/GOLANG/{v} Yes
Telemetry push Yes
Thrift No (uses c.client directly; already routes via URL) — (not needed — URL-routed)
OAuth token exchange No (separate client, talks to login.microsoftonline.com) — (not needed)
CloudFetch / Volume operations No (presigned URLs) — (not needed)

Verification

  • go build ./... clean.
  • Debug log output confirms extraction on SPOG URLs:
    SPOG header extraction: injecting x-databricks-org-id=<id> (extracted from ?o= in httpPath)
    

Note on the earlier auth/oauth/u2m/u2m.go change

The two earliest commits on this branch (23697e5, 0ec7e06) modified auth/oauth/u2m/u2m.go to avoid sending an empty client_secret on the PKCE public-app flow, documented as a fix for the server's "Public app should not use a client secret" rejection. That change was empirically verified to not be needed (see commit 3576c92 which reverts it):

  • Prod Legacy (adb-6436897454825492.12.azuredatabricks.net): Unpatched u2m.go PASSES end-to-end. Server accepts the empty client_secret.
  • Stg Legacy (adb-7064161269814046.2.staging.azuredatabricks.net): Unpatched u2m.go FAILS with unexpected HTTP status 400 Bad Request during token exchange.

Since production tolerates the current behavior, customers aren't impacted. Keeping this PR minimal; if staging-level strictness later rolls out to prod, we can re-add the u2m fix then.

Test plan

  • Unit tests pass (go test ./...)
  • Manually verified SPOG header injection appears in x-databricks-org-id on feature-flag check and telemetry push against a SPOG workspace (not gated by this PR, but prerequisite for SPOG telemetry to work at all)
  • No regressions on non-SPOG workspaces (wrapper is a no-op when extractSpogHeaders returns nil)

The U2M flow uses PKCE (public app) and should not send a client secret.
Previously, ClientSecret was always set to "" on the oauth2.Config, which
caused Go's oauth2 library to send an empty client_secret via Basic auth.
The OIDC server rejects this with "Public app should not use a client
secret".

Only set ClientSecret when it's non-empty, so public apps use the "none"
token endpoint auth method as intended.

Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>

Co-authored-by: Isaac
Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
Feature flags:
- Fix endpoint path: /api/2.0/feature-flags -> /api/2.0/connector-service/feature-flags/GOLANG/{version}
- Fix response parsing: map format -> array of {name, value} entries
- Add extraHeaders for SPOG routing (x-databricks-org-id)
- Extract ?o=<workspaceId> from httpPath in connector

U2M OAuth:
- Don't set ClientSecret for public apps (PKCE)
- Force AuthStyleInParams to prevent Basic auth with empty password
- Server rejects "Public app should not use a client secret" otherwise

Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>

Co-authored-by: Isaac
Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
Mirrors equivalent logging added to OSS JDBC and pysql. Emits at DEBUG
level in three paths of extractSpogHeaders:

1. Malformed query string in httpPath — log and skip.
2. httpPath has "?" but no ?o= param — log and skip.
3. Injection happens — log the extracted workspace ID so customers
   diagnosing SPOG routing can confirm the header was added.

Also adds a detailed docstring explaining the role this header plays:
Thrift routing stays URL-driven via ?o= in httpPath; only the separate
endpoints (telemetry, feature flags) need the header for account-level
routing on SPOG hosts.

Helps with customer support: when a customer reports "SPOG isn't routing
correctly", they can enable DEBUG logging and immediately see whether
the driver saw their ?o= value.

Signed-off-by: Madhavendra Rathore
Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
# Conflicts:
#	connector.go
#	telemetry/config.go
#	telemetry/config_test.go
#	telemetry/driver_integration.go
#	telemetry/featureflag.go
#	telemetry/featureflag_test.go
… API churn

Replace the per-function extraHeaders parameter threading (added during
the previous merge with main) with a minimal http.Client wrapper in the
top-level dbsql package.

Before (5 files, including 3 in telemetry/*):
  connector.go extracted ?o=<workspaceId>, then passed it as an
  ExtraHeaders field through telemetry.TelemetryInitOptions →
  isTelemetryEnabled → featureFlagCache.isTelemetryEnabled →
  fetchFeatureFlag, where it was applied to the outbound request.
  The telemetry push path (telemetry/exporter.go) was NOT covered.

After (2 files — connector.go and auth/oauth/u2m/u2m.go):
  connector.go extracts ?o= as before, but now wraps the driver's
  *http.Client with a headerInjectingTransport that sets the SPOG
  header on every outbound request through that client. Passes the
  wrapped client (not c.client) into TelemetryInitOptions.HTTPClient.

Advantages:
  - telemetry/*.go files revert to identical-to-main. No API churn.
  - Both feature-flag and telemetry-push paths automatically get the
    SPOG header (previously only feature-flag did).
  - Future HTTP paths that reuse the telemetry http.Client inherit
    SPOG routing for free.

Thrift is unaffected: it uses c.client directly (not the wrapper) and
routes via ?o= in the URL path. The transport wrapper is only applied
to the HTTP client handed to telemetry.

Signed-off-by: Madhavendra Rathore
Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
Earlier commits in this branch (23697e5, 0ec7e06) modified u2m.go to
avoid sending an empty client_secret on the PKCE public-app flow,
citing server rejection with "Public app should not use a client secret".

Empirical verification (2026-04-21):
  - Prod Legacy  (adb-6436897454825492.12.azuredatabricks.net): PASS with
    unpatched u2m.go — server accepts the request.
  - Stg Legacy   (adb-7064161269814046.2.staging.azuredatabricks.net):
    FAIL with 400 Bad Request on unpatched u2m.go.

Since the production server tolerates the current behavior, the patch
isn't strictly required for customers. Reverting to keep the PR minimal
and matching upstream main exactly for this file. If staging server
strictness later rolls out to prod, we can re-add this fix then.

Signed-off-by: Madhavendra Rathore
Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
@msrathore-db msrathore-db changed the title Fix feature flags, U2M OAuth, and add SPOG header support Add SPOG (Custom URL) routing support via x-databricks-org-id header Apr 21, 2026
Covers extractSpogHeaders (8 cases: missing/empty query, valid o=,
missing o=, empty value, multi-param, duplicate o=, bare `?`) and
headerInjectingTransport (injection, caller-set not overridden, other
headers preserved, original client untouched).

Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
Copy link
Copy Markdown
Collaborator

@samikshya-db samikshya-db left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, please add in nodejs too 🙏

@msrathore-db msrathore-db merged commit 12c3f2a into main Apr 21, 2026
3 checks passed
@msrathore-db msrathore-db deleted the spog-fixes branch April 21, 2026 10:54
@vikrantpuppala vikrantpuppala mentioned this pull request Apr 21, 2026
2 tasks
vikrantpuppala added a commit that referenced this pull request Apr 21, 2026
## Summary
Bump `DriverVersion` to `1.11.0` and add the v1.11.0 section to
`CHANGELOG.md`.

### Changes since v1.10.0
- Enable telemetry by default with DSN-controlled priority (#320, #321,
#322, #349)
- Add SPOG (Custom URL) routing support via `x-databricks-org-id` header
(#347)
- Add statement-level query tag support (#341)
- Add AI coding agent detection to User-Agent header (#326)
- Fix CloudFetch returning stale column names from cached results (#351)
- Fix resource leak: close staging Rows in execStagingOperation (#325)

Internal/infra-only changes are omitted from the user-facing notes (CI
hardening, dependabot bumps, CODEOWNERS).

## Test plan
- [x] `go build ./...` clean
- [x] `go test ./... -count=1 -short` passes locally

## Next steps after merge
1. Tag the merge commit as `v1.11.0` and push the tag
2. Trigger `peco-databricks-sql-go` in
secure-public-registry-releases-eng with `ref=v1.11.0`, `dry-run=true`
to verify
3. Re-run with `dry-run=false` for the actual release

NO_CHANGELOG=true

This pull request was AI-assisted by Isaac.

Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants