{"record":{"id":"01056655de49bf0b","repo":"Tencent/WeKnora","slug":"resource-release-requires-owner-type-and-id","errorCode":null,"errorMessage":"resource release requires owner type and id","messagePattern":"resource release requires owner type and id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/application/service/resource.go","lineNumber":168,"sourceCode":"\t\tResourceID: resource.ID,\n\t\tTenantID:   resource.TenantID,\n\t\tOwnerType:  ownerType,\n\t\tOwnerID:    ownerID,\n\t\tRelation:   relation,\n\t})\n}\n\n// Release implements interfaces.ResourceCatalog.\n//\n// Unbinding and counting are deliberately not a single transaction. A racing\n// bind that lands between them makes the count too high, which keeps a live\n// file — the safe direction. The opposite ordering could delete bytes another\n// owner had just claimed.\nfunc (s *resourceCatalog) Release(\n\tctx context.Context, reference, ownerType, ownerID string,\n) (int64, error) {\n\tif strings.TrimSpace(ownerType) == \"\" || strings.TrimSpace(ownerID) == \"\" {\n\t\treturn -1, fmt.Errorf(\"resource release requires owner type and id\")\n\t}\n\tif _, ok := types.ParseResourcePath(reference); !ok {\n\t\t// A raw provider path predates the catalog and has no bindings to\n\t\t// account for; the caller keeps its previous delete behaviour.\n\t\treturn -1, nil\n\t}\n\tresource, err := s.Resolve(ctx, reference)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\tif err := s.repo.DeleteBinding(ctx, resource.ID, ownerType, ownerID); err != nil {\n\t\treturn -1, err\n\t}\n\treturn s.repo.CountBindings(ctx, resource.ID)\n}\n\nfunc (s *resourceCatalog) MarkDeleted(ctx context.Context, reference string) error {\n\tresource, err := s.Resolve(ctx, reference)","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/application/service/resource.go#L150-L186","documentation":"Release mirrors Bind's requirement: it needs ownerType and ownerID to identify whose bindings to remove. Blank owner fields abort with this error before any lookup. Note the deliberate contrast — an unparseable reference returns (‑1, nil) for legacy paths, but a blank owner is always a hard error.","triggerScenarios":"Calling Release with empty or whitespace ownerType/ownerID, typically when the owning entity was never resolved or the owner struct is zero-valued.","commonSituations":"Cleanup jobs running without owner context; API handlers dropping owner fields during deserialization; tests calling Release with placeholder empty values.","solutions":["Pass the same ownerType/ownerID used in the original Bind call","Validate owner fields before invoking Release","If ownership is unknown, resolve it first via bindings lookup instead of releasing with blanks","Return a 400-style validation error to callers who omit owner metadata"],"exampleFix":"// before\nremoved, err := catalog.Release(ctx, ref, owner.Type, owner.ID)\n// after\nif strings.TrimSpace(owner.Type) == \"\" || strings.TrimSpace(owner.ID) == \"\" {\n    return fmt.Errorf(\"cannot release without owner identity\")\n}\nremoved, err := catalog.Release(ctx, ref, owner.Type, owner.ID)","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(ownerType) == \"\" || strings.TrimSpace(ownerID) == \"\" {\n    return errors.New(\"owner identity required for release\")\n}","typeGuard":"func releaseable(ref, ownerType, ownerID string) bool {\n    _, ok := types.ParseResourcePath(ref)\n    return ok && strings.TrimSpace(ownerType) != \"\" && strings.TrimSpace(ownerID) != \"\"\n}","tryCatchPattern":"n, err := catalog.Release(ctx, ref, ownerType, ownerID)\nif err != nil && strings.Contains(err.Error(), \"requires owner type and id\") {\n    return ErrMissingOwner\n}","preventionTips":["Thread owner identity through cleanup jobs, don't default to empty strings","Mirror the same owner values used in Bind","Validate owner fields before any catalog mutation","Reject empty owner metadata at the handler layer"],"tags":["input-validation","release"],"backgroundTag":"missing-required-argument","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}