Fix checkins answer update returning 422 validation error#228
Fix checkins answer update returning 422 validation error#228jeremy merged 9 commits intobasecamp:mainfrom
Conversation
The BC3 Question::Answer model validates presence of both content and group_on. The controller rebuilds the recordable from params on update rather than merging with the existing record, so any field not re-submitted is nil and fails validation. UpdateAnswer was only sending content, causing a 422 on every call. Fix: when GroupOn is not explicitly provided, fetch the existing answer first and carry its group_on forward in the PUT body. Also switch to WithBody to bypass the generated type which lacks the field. Remove the unused wrapper types (createAnswerRequestWrapper, updateAnswerRequestWrapper) that were previously added but never wired in.
There was a problem hiding this comment.
Pull request overview
Fixes CheckinsService.UpdateAnswer returning 422s by ensuring group_on is always sent (either explicitly from the caller or preserved from the existing answer), working around a generated request type that lacks group_on.
Changes:
- Adds
GroupOntoUpdateAnswerRequestand updates marshaling tests. - Updates
UpdateAnswerto fetch the existing answer whenGroupOnis omitted and to send a custom JSON body includinggroup_on. - Removes unused create/update wrapper types and replaces wrapper tests with direct JSON body tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| go/pkg/basecamp/checkins.go | Adds GroupOn to update requests and updates UpdateAnswer to preserve/send group_on via a custom body. |
| go/pkg/basecamp/checkins_test.go | Updates marshaling tests and adds service-level tests covering preserved vs explicit group_on behavior. |
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Review carefully before merging. Consider a major version bump. |
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="go/pkg/basecamp/helpers.go">
<violation number="1" location="go/pkg/basecamp/helpers.go:54">
P1: Retries after partial body reads can resend from the middle of the payload because the reader only rewinds after EOF. Reset on request close so every retry starts at byte 0.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
jeremy
left a comment
There was a problem hiding this comment.
High: Spec gap — Go-only patch, all other SDKs still broken
QuestionAnswerUpdatePayload in spec/basecamp.smithy:6254-6257 has only content. The create counterpart (QuestionAnswerPayload, line 6215-6222) correctly includes group_on. The update payload needs the same field.
Because the spec is the source of truth for codegen across all SDKs, this omission means TypeScript, Ruby, Python, Kotlin, and Swift all have the identical bug. Evidence from each SDK's generated update-answer code:
typescript/src/generated/services/checkins.ts:183—updateAnswersends onlycontentpython/src/basecamp/generated/services/checkins.py:26—update_answersignature has nogroup_onruby/lib/basecamp/generated/services/checkins_service.rb:31—update_answer(answer_id:, content:)onlyswift/Sources/Basecamp/Generated/Models/QuestionAnswerUpdatePayload.swift:4— onlycontent: Stringkotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/checkins.kt:60— JSON body has only"content"
The Go fix hand-builds a map[string]any and calls UpdateAnswerWithBodyWithResponse, bypassing the generated typed body entirely. This works for Go but leaves five SDKs broken.
Fix: Add group_on: ISO8601Date to QuestionAnswerUpdatePayload in Smithy, regenerate OpenAPI, regenerate all SDKs. The Go marshalBody pattern is still needed (to avoid types.Date{} serializing as null on partial updates), but it should be fixing a zero-value problem, not compensating for a missing spec field.
marshalBody doc comment (helpers.go:26-34): CheckinsService.UpdateAnswer should be added to the method list since it now uses this pattern.
Medium: rewindableReader generalization in marshalBody
Previously marshalBody returned bytes.NewReader(b). Now it returns &rewindableReader{data: b}. This changes request framing for all 8+ callers.
What improves
doWithRetry (generated client.gen.go:3126) re-calls buildRequest() on each attempt, passing the same body io.Reader. With bytes.NewReader, after the first attempt reads to EOF, retries get an empty body — a pre-existing silent bug. rewindableReader auto-rewinds after EOF, so retries get the full payload. This is genuinely better for the full-read-then-retry path.
What regresses
http.NewRequest only special-cases *bytes.Buffer, *bytes.Reader, and *strings.Reader (see net/http/request.go:923). With *rewindableReader, ContentLength stays 0 with a non-nil body and no GetBody, which the transport treats as unknown length and sends chunked over HTTP/1.1.
*bytes.Reader |
*rewindableReader |
|
|---|---|---|
ContentLength |
actual length | 0 with non-nil body (unknown length) |
GetBody |
snapshot closure | nil (no redirect replay) |
Partial-read on retry
If a network failure interrupts mid-body-write, both readers leave the position in the middle. Neither rewinds. doWithRetry calls buildRequest() again with the same reader, so the retry sends only the unread suffix. This is equally broken for both implementations — not a regression, but not fixed either.
Test gap
TestMarshalBody_ReturnsReplayableReader (helpers_test.go:151) proves EOF-to-EOF replay with io.ReadAll. It does not cover the actual doWithRetry retry path, the partial-read case, or the changed request framing.
Fix options: Either fix retries centrally so each attempt gets a fresh *bytes.Reader/GetBody from raw bytes, or keep the current body builder and accept the framing change explicitly. Do not rely on adding Len() int to rewindableReader — http.NewRequest will not use it.
Low: Cleanup is clean
- Wrapper types (
createAnswerRequestWrapper,updateAnswerRequestWrapper) were dead code. Removal is correct. rewindableReadermove fromaccount_service.gotohelpers.gocompiles (same package).UpdateAnswerRequest.GroupOnwithomitemptycorrectly controls auto-fetch vs explicit path.- Four new test cases cover the key scenarios: auto-preserve, explicit, invalid format, missing resolved. Test design is good.
Upstream contract inconsistency
The public API docs (bc3-api/sections/question_answers.md) show PUT /question_answers/:id with only content in the body, while the Rails controller and model require group_on. Decide which contract is authoritative, then update Smithy/OpenAPI/docs/tests to match.
Recommendation
- Add
group_ontoQuestionAnswerUpdatePayloadin Smithy, regenerate OpenAPI + all SDKs - Either fix retries centrally so each attempt gets a fresh
*bytes.Reader/GetBodyfrom raw bytes, or keep the current body builder and accept the framing change explicitly - Add
CheckinsService.UpdateAnswerto themarshalBodydoc comment - Coordinate with the bc3-api docs to clarify whether
group_onis required on update
The BC3 Question::Answer model validates presence of group_on, but QuestionAnswerUpdatePayload only had content. This caused all SDKs to fail with a 422 on answer updates. The create counterpart (QuestionAnswerPayload) already included group_on — the update payload was simply missing it.
Go, TypeScript, Ruby, Python, Kotlin, and Swift now include group_on in the update answer request type and body.
Ruby: UpdateAccountLogo retry metadata, UpdateMyProfile retry attempts, GaugeNeedle.comment_count removal. Swift: gauges, my-assignments, my-notifications, account, and people operations added in prior spec revisions.
Verify that group_on is sent in the request body when provided and omitted when not.
Spec Change Impact
SDKs Needing Updates:
All SDKs require regeneration to incorporate the fix. |
|
All four items from the review have been addressed:
Additionally: added update-answer |
Resolve conflicts in generated files (Ruby metadata/types, Swift Account model/service, TypeScript metadata) by regenerating from the merged OpenAPI spec.
There was a problem hiding this comment.
1 issue found across 62 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="python/tests/services/test_checkins.py">
<violation number="1" location="python/tests/services/test_checkins.py:51">
P1: This test asserts the old broken behavior by requiring `group_on` to be omitted, so it won’t catch regressions of the 422 fix that should always send `group_on` on update.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 24 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
UpdateAnswerfails with a generic validation error on every call, regardless of content.Ref: https://3.basecamp.com/2914079/buckets/46292715/card_tables/cards/9714931651
Docs Update: https://github.com/basecamp/bc3/pull/10075
Root cause
The BC3
Question::Answermodel requires bothcontentandgroup_on:The controller rebuilds the recordable from params rather than merging with the existing record:
The SDK was only sending
content, sogroup_onwas nil → validation failure → 422.Fix
UpdateAnswernow auto-fetches the existing answer to carry itsgroup_onforward when the caller doesn't provide oneGroupOntoUpdateAnswerRequestso callers can explicitly override itWithBodyto bypass the generated type which lacks the fieldcreateAnswerRequestWrapperandupdateAnswerRequestWrappertypes that were defined and tested but never wired inSummary by cubic
Fixes 422 errors when updating Basecamp check-in answers by always sending the required
group_on. Updates keep the existing date when not provided and accept an explicit override.Bug Fixes
group_onon update: auto-fetch when absent, validateYYYY-MM-DD, and error if missing/invalid.WithBodypath with a replayable body socontent+group_onare reliably sent.group_oninQuestionAnswerUpdatePayloadand regenerate SDKs (Go/TypeScript/Ruby/Python/Kotlin/Swift); tests verifygroup_onis sent when provided and omitted when not.Refactors
helpersviamarshalBody.Written for commit a11dafa. Summary will update on new commits.