feat: add WithHTTPClient/WithEventContext ClientOptions; fix: WithContext receiver mutation - #29
Open
fatmcgav wants to merge 3 commits into
Open
feat: add WithHTTPClient/WithEventContext ClientOptions; fix: WithContext receiver mutation#29fatmcgav wants to merge 3 commits into
WithHTTPClient/WithEventContext ClientOptions; fix: WithContext receiver mutation#29fatmcgav wants to merge 3 commits into
Conversation
`slacker.NewClient` built its internal `*slack.Client` via `slack.New`, but
`newSlackOptions` only ever forwarded `slack.OptionDebug`,
`slack.OptionAppLevelToken`, and optionally `slack.OptionAPIURL`. Every field
on `Slacker` and `*slack.Client` is unexported with no setters, so there was
no way to control the HTTP transport slacker uses, even though
`slack-go/slack` already supports it via `slack.OptionHTTPClient`.
This blocked consumers who want to instrument outbound Slack Web API calls —
e.g. OpenTelemetry's `otelhttp` transport for tracing/metrics, request
logging, custom retry/timeout behaviour, or proxying — since they had no
handle on the transport at all.
Add a `WithHTTPClient` `ClientOption` that plumbs a caller-supplied
`*http.Client` through to `slack.New` via `slack.OptionHTTPClient`, forwarded
conditionally from `newSlackOptions` in `slacker.go`. Purely additive: when
the option isn't passed, `HTTPClient` stays `nil` and behaviour is unchanged
from today (`slack-go/slack` falls back to its own bare `&http.Client{}`).
Adds `options_test.go` (no test files previously existed in this repo)
covering the new default, `WithHTTPClient` setting the field, and
`newSlackOptions` only appending `slack.OptionHTTPClient` when set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`CommandContext.WithContext`, `InteractionContext.WithContext`, and `JobContext.WithContext` were documented with the standard `http.Request.WithContext` contract — "returns a shallow copy of r with its context changed to ctx" — but didn't honour it: ```go r2 := new(CommandContext) *r2 = *r r.ctx = ctx // mutates the receiver, not the copy return r // returns the receiver, not the copy ``` Two bugs, and the second survives fixing the first: 1. It mutated `r` and returned `r` — `r2` was built and discarded, silently changing state any other holder of the original pointer would see. 2. Even copying correctly, `response` was a shallow-copied pointer to the *same* `*ResponseReplier`/`*ResponseWriter`, which wraps a `*Writer` whose `ctx` is captured once at construction (`newWriter(ctx, ...)`). So `WithContext` changed what `Context()` reported but not what context `Reply`/`ReplyError`/`ReplyBlocks`/`Post*` actually issued their Slack API calls against — they kept using the original, stale context. This defeated any caller attaching a per-request context (e.g. one carrying an OpenTelemetry span) after construction. Fix all three `WithContext` methods in `context.go` to build a real independent copy, rebuilding the `Writer`/`Replier`/`response` against the new `ctx` via the same unexported constructors `newCommandContext`/ `newInteractionContext`/`newJobContext` already use. Adds `context_test.go` covering, for all three context types: - `WithContext` returns a different pointer and does not mutate the receiver's context (regression test for bug slack-io#1). - After `WithContext`, `Response().Reply`/`Post` issues its Slack API call with the *new* context, not the original (regression test for bug slack-io#2) — verified via a fake `http.RoundTripper` wired in through the `WithHTTPClient` option (added earlier on this branch), which lets the test observe the exact `context.Context` slack-go attaches to the outgoing `*http.Request`. Confirmed both new assertions fail against the pre-fix code and pass with the fix applied. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WithHTTPClient ClientOptionWithHTTPClient ClientOption; fix: WithContext receiver mutation
…ntext` `handleMessageEvent` discarded the caller's `ctx` before enriching a raw event via `newMessageEvent` (`message_event.go`), so `getChannel`, `getUserProfile`, and `ignoreBotMessage` called `GetConversationInfo`, `GetUserInfo`, and `GetBotInfo` — the non-`Context` variants — with no way to attach a caller's context/span. Callers instrumenting outbound calls via `WithHTTPClient` (e.g. an `otelhttp`-wrapped client) saw these three calls surface as disconnected trace roots. - Thread `ctx` through `newMessageEvent` → `getChannel`/`getUserProfile`, and into `ignoreBotMessage`, switching each to its `Context` variant (`GetConversationInfoContext`, `GetUserInfoContext`, `GetBotInfoContext`). - Add `WithEventContext`, a `ClientOption` taking an `EventContextFunc` that lets a caller derive a per-event context (e.g. one carrying a span) before this enrichment runs, with a cleanup func deferred until dispatch for that event has fully returned. Wired into both `handleMessageEvent` and `handleInteractionEvent`, nil-guarded so behaviour is unchanged when the option isn't set. All touched functions are unexported and single-call-site, so this is additive with no consumer-facing signature break. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WithHTTPClient ClientOption; fix: WithContext receiver mutationWithHTTPClient/WithEventContext ClientOptions; fix: WithContext receiver mutation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WithHTTPClientClientOptionslacker.NewClient(botToken, appToken, ...ClientOption)builds its internal*slack.Clientviaslack.New(botToken, slackOpts...), butnewSlackOptionsinslacker.goonly ever passesslack.OptionDebug,slack.OptionAppLevelToken, and optionallyslack.OptionAPIURL. There is noClientOptionthat lets a caller supply a custom*http.Client(orhttp.RoundTripper), even though the wrappedgithub.com/slack-go/slackpackage already supports this viaslack.OptionHTTPClient.Every field on
Slacker(and on*slack.Client) is unexported with no setters, so today the only way to control the HTTP transport slacker uses is at construction time, through a newClientOption. This blocks consumers who want to instrument outbound Slack Web API calls — e.g. wiring up OpenTelemetry'sotelhttptransport for tracing/metrics, request logging, custom retry/timeout behaviour, or proxying — since they cannot get a handle on the transport at all.Adds a
WithHTTPClientClientOptionthat plumbs a caller-supplied*http.Clientthrough toslack.Newviaslack.OptionHTTPClient:options.go: newHTTPClient *http.Clientfield onclientOptions, and theWithHTTPClientoption.slacker.go:newSlackOptionsforwardsslack.OptionHTTPClient(...)only whenHTTPClientis set.Purely additive and backward-compatible: when the option isn't passed,
HTTPClientstaysniland behaviour is unchanged —slack-go/slackfalls back to its own bare&http.Client{}, exactly as it does today.Fix:
WithContextmutated the receiver instead of returning a copyWhile adding tests for the option above, I found
CommandContext.WithContext,InteractionContext.WithContext, andJobContext.WithContext(context.go) are documented with the standardhttp.Request.WithContextcontract — "returns a shallow copy ofrwith its context changed toctx" — but don't honour it:Two bugs, and the second survives fixing the first:
rand returnsr—r2is built and discarded, silently changing state any other holder of the original pointer would see.responseis a shallow-copied pointer to the same*ResponseReplier/*ResponseWriter, wrapping a*Writerwhosectxis captured once at construction (newWriter(ctx, ...)). SoWithContextchanged whatContext()reported but not what contextReply/ReplyError/ReplyBlocks/Post*actually issued their Slack API calls against — they kept using the original, stale context. This defeated any caller attaching a per-request context (e.g. one carrying an OpenTelemetry span) after construction.Fixed all three
WithContextmethods to build a real independent copy, rebuilding theWriter/Replier/responseagainst the newctxvia the same unexported constructorsnewCommandContext/newInteractionContext/newJobContextalready use.Thread
ctxthrough message-event enrichment; addWithEventContexthandleMessageEventis the single entry point for both free-text messages and slash commands, but before any command is matched it callednewMessageEvent(message_event.go) — which enriches the raw event viagetChannel/getUserProfile— with noctxat all, even thoughhandleMessageEvent(ctx, event)already had one in scope. Those two helpers, plusignoreBotMessage's bot-identity lookup, called the non-ContextSlack API variants (GetConversationInfo,GetUserInfo,GetBotInfo). Combined withWithHTTPClientabove, a caller instrumenting outbound calls (e.g. viaotelhttp) would see these three calls surface as disconnected trace roots — there was no way to attach a context/span before slacker started its own pre-dispatch work.Threaded
ctxthroughnewMessageEvent→getChannel/getUserProfile, and intoignoreBotMessage, switching each to itsContextvariant (GetConversationInfoContext,GetUserInfoContext,GetBotInfoContext).Added
WithEventContext, aClientOptiontaking anEventContextFuncthat lets a caller derive a per-event context (e.g. one carrying a span) before this enrichment runs:The returned cleanup func is deferred until dispatch for that event — enrichment, command/interaction routing, and the matched handler — has fully returned, and is called via both
handleMessageEventandhandleInteractionEvent, nil-guarded so behaviour is unchanged when the option isn't set.All touched functions (
newMessageEvent,getChannel,getUserProfile,ignoreBotMessage) are unexported and single-call-site, so this is additive with no consumer-facing signature break.Tests
This repo had no test files at all before this PR, so all of the following are new:
options_test.go: coversWithHTTPClient's default (nil), that it sets the field, and thatnewSlackOptionsonly appendsslack.OptionHTTPClient(...)when set.context_test.go: for all three context types —WithContextreturns a different pointer and does not mutate the receiver's context (regression test for bug 1).WithContext,Response().Reply/Postissues its Slack API call with the new context, not the original (regression test for bug 2) — verified via a fakehttp.RoundTripperwired in throughWithHTTPClient, letting the test observe the exactcontext.Contextslack-go attaches to the outgoing*http.Request.event_context_test.go: for bothhandleMessageEventandhandleInteractionEvent—WithEventContextset, enrichment/handler Slack API calls carry the caller's originalctx(regression test proving the drop is fixed).WithEventContextset, those calls carry the derived context, and the cleanup func runs exactly once, after dispatch has fully returned.Confirmed the
context_test.goassertions fail against the pre-fix code and pass with the fix applied.I didn't touch the README or add a changelog entry — this repo doesn't currently document
ClientOptions individually in the README, and there's noCHANGELOG.mdto update, so there was no existing convention to extend. Happy to add either if maintainers would like.🤖 Generated with Claude Code