Dedupe observable/entity/indicator tag endpoint bodies (item #5, first slice) - #1333
Merged
Conversation
…ared helper
The observable, entity, and indicator routers each independently
implemented the exact same /tag endpoint body: look up every requested
ID (404/400 on any miss), then tag each found object and record its
tags. The only real per-type differences were the not-found status
code (observable uses 400, entity/indicator use 404 -- an existing,
unintentional inconsistency, preserved here rather than silently
"fixed" as part of a refactor) and the wording of the not-found detail
message.
Add core/web/apiv2/tagging.py with a single tag_objects() helper,
parameterized by base type (mirroring the existing context.py pattern
for add_context/replace_context/delete_context) plus the two points of
per-type variation. Each router's endpoint is now a few lines that call
the helper and wrap the result in its own typed response model -- the
request/response Pydantic models and route decorators are untouched, so
the generated OpenAPI spec is byte-identical (verified via a diffed
throwaway build).
Incidental fix: observable's tag endpoint built its not-found detail
message without an f-string prefix, so it literally sent the string
"ID:{observable_id}" instead of the real ID. Fixed as part of moving
this logic into the shared helper, since preserving it faithfully in
the callback-based extraction would have required going out of the way
to keep it broken. Added a regression test for this specific message
(tests/apiv2/observables.py) -- verified it fails on the pre-fix router
and passes with the fix.
First slice of the architecture review's item #5 (observable/entity/
indicator router triplication). The recommended remaining slices --
search endpoint body, get-by-name, details, delete -- are structurally
similar candidates for the same treatment in follow-up PRs.
This was referenced Jul 30, 2026
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.
Summary
First incremental slice of the architecture review's item #5 (the
observable/entity/indicator router triplication) — the
/tagendpointbody, one of the four behaviors the review names as extraction
candidates ("search endpoint body, tag endpoint body, context
endpoints, bulk-add loop").
The observable, entity, and indicator routers each independently
implemented the exact same
/tagendpoint logic: look up everyrequested ID (raising on any miss), then tag each found object and
record its resulting tags. The only genuine per-type differences were:
404 (an existing, unintentional inconsistency — preserved here
exactly rather than silently "fixed" as a side effect of a refactor)
Change
Added
core/web/apiv2/tagging.pywith a singletag_objects()helper,parameterized by base type and the two per-type variation points above
— mirroring the existing pattern in
core/web/apiv2/context.py(
add_context/replace_context/delete_context, already generic overType[T]). Each router's/tagendpoint is now a few lines that callthe helper and wrap the result in its own typed response model. The
request/response Pydantic models and route decorators are untouched in
all three routers.
OpenAPI spec verified byte-identical: generated the spec from a
throwaway build of the pre-change routers and diffed it (md5) against
the post-change build — identical.
Incidental fix: observable's
/tagendpoint built its not-founddetail message without an
fprefix, so on a missing ID it literallyreturned the string
"...ID:{observable_id}"instead of the real ID.Preserving this exactly would have meant deliberately keeping a broken
string in the new shared helper's callback, which seemed worse than
just fixing it — so it's fixed here, called out explicitly rather than
silently bundled in. Doesn't change status code or response shape, only
the
detailstring content on an already-erroring request.Test plan
test_tag_observable_unknown_id(
tests/apiv2/observables.py) covering the incidental fix —verified it fails on the pre-fix router (asserts the literal
broken string) and passes with the fix.
tests/apiv2suite: 200/202 pass (same 2 known pre-existingfailures in
tests/apiv2/tasks.py, unrelated).tests/schemas(190/190) andtests/core_tests(31/31) passunchanged.
ty check(core+yetictl and plugins jobs): 0 errors.ruff check/ruff format --check: clean.Scope note
This is deliberately just the tag endpoint. The review's other named
candidates (search endpoint body,
get-by-name,details,delete,bulk-add) are structurally similar but each has its own small
asymmetries worth checking individually (e.g. observable's
searchlacks the
filter_aliases/get_multiplesupport entity/indicatorhave) — better done as separate, individually-reviewable follow-ups
than one large PR.