---
title: "Handling Failures"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Handling Failures}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(getaca)

.old_options <- options(getaca.cache = file.path(tempdir(), "getaca-failures"))
.old_envvars <- Sys.getenv(c("GETACA_OFFLINE", "NOT_CRAN"), unset = NA)
Sys.setenv(GETACA_OFFLINE = "true", NOT_CRAN = "true")

reg <- registry("yourpkg", list(
  resource("backbone", "2026-06",
           urls = c("https://primary.invalid/backbone-2026-06.zip",
                    "https://mirror.invalid/backbone-2026-06.zip"),
           sha256 = strrep("9f", 32), size = 797e6, license = "CC-BY-4.0")
))
```

A plain downloader reports eleven different situations the same way: something
went wrong with a URL. `getaca` separates them, because the eleven have three
different people who can act on them and five different things to do.

Every failure is a classed condition carrying an `actor` field naming who can
act. Callers branch on the class rather than on message text.

| Condition | Cause | Actor | Action |
|---|---|---|---|
| `getaca_error_unavailable` | no mirror answered | user | connect, or prefetch elsewhere |
| `getaca_error_offline` | not cached, network not permitted here | user | prefetch, or point `GETACA_CACHE` at a seeded cache |
| `getaca_error_incomplete` | every mirror ended short | user | retry |
| `getaca_error_credentials` | every mirror refused to serve | user, or author | set the credential named; where none is declared, report it |
| `getaca_error_upstream_changed` | published bytes were replaced | upstream | the declaring package needs a new record |
| `getaca_error_cache_corrupt` | local copy drifted from its own record | user | clean and refetch |
| `getaca_error_redeclared` | the declaration names different bytes for a version already held | author | report to the declaring package |
| `getaca_error_invalid_registry` | the declaration is wrong | author | report to the declaring package |
| `getaca_error_declaration` | every mirror agrees, the registry disagrees | author | report to the declaring package |
| `getaca_error_composition` | the parts arrived intact and compose to something else | author | report to the declaring package |
| `getaca_error_signature` | a registry that must be signed carried no usable one | author | report to the declaring package |

All eleven inherit from `getaca_error`, which inherits from `error`, so a
handler for `getaca_error` catches everything the package raises and nothing
else.

## The general shape

```{r}
err <- tryCatch(getaca("backbone", registry = reg), getaca_error = function(e) e)
class(err)
```

```{r}
err$actor
```

```{r}
cat(conditionMessage(err))
```

Three parts to every message: what happened, what to do about it, and a
closing line derived from the actor. The `Fix:` line is what tells a user
whether the next step is theirs, and the `Action:` lines above it are what
they type.

Beyond `actor`, each condition carries the data that produced it, so a handler
can reason about the situation rather than parse the message:

```{r}
names(err)
```

`err$id` is the `getaca_id`, so `format(err$id)` gives
`package/name@version` for a log line.

## `getaca_error_unavailable`

No mirror could be reached. Every URL is listed with the reason it failed, so
a user can tell a proxy problem from a host that is down from a path that
404s.

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: Cannot reach any source for yourpkg/backbone@2026-06.
#>   https://primary.invalid/backbone-2026-06.zip: HTTP 503
#>   https://mirror.invalid/backbone-2026-06.zip: Could not resolve host
#>
#> Actions: connect to a network; or run getaca_prefetch() on a connected
#> machine and copy the cache directory; or set the resource optional here
#> with getaca_available() before calling.
#>
#> Fix: see the actions listed above.
```

The condition carries `urls` and `reasons` as parallel vectors, which is
enough to build a domain-specific message:

```{r, eval = FALSE}
tryCatch(
  getaca("backbone", package = "yourpkg"),
  getaca_error_unavailable = function(e) {
    stop(sprintf(
      "Could not download the backbone from %d source(s).\nTried:\n%s",
      length(e$urls), paste0("  ", e$urls, " (", e$reasons, ")", collapse = "\n")
    ), call. = FALSE)
  }
)
```

## `getaca_error_offline`

A subclass of `getaca_error_unavailable`, raised when the resource is not
cached and the policy in force forbids reaching for it. Under `R CMD check`
this is the only failure a resource-dependent code path can produce, because
the clamp forces `offline` before any transport is attempted.

```{r}
cat(conditionMessage(err))
```

The subclass relationship is what lets a package write one handler and get
both:

```{r}
inherits(err, "getaca_error_unavailable")
```

Separating them is worth doing when the two need different words. "You are
offline" is wrong advice for a machine that is online but running a check, and
"the server is down" is wrong advice for a machine with no network at all.

```{r, eval = FALSE}
tryCatch(
  getaca("backbone", package = "yourpkg"),
  getaca_error_offline = function(e) {
    message("Running without network access. Prefetch first:\n",
            "  getaca_prefetch(\"backbone\", package = \"yourpkg\")")
    NULL
  },
  getaca_error_unavailable = function(e) {
    message("The download sources are not responding. Try again later.")
    NULL
  }
)
```

Handlers are matched in order, so the narrower one has to come first.

## `getaca_error_incomplete`

Every mirror that failed did so by ending short of the declared size. That is
a different situation from a host being down, and the action is different:
retry, because a partial transfer resumes rather than restarting.

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: Transfer of yourpkg/backbone@2026-06 ended early.
#>   expected 797000000 bytes, received 412300000 bytes
#>
#> Action: retry. Any previously cached copy was left untouched.
#>
#> Fix: see the actions listed above.
```

The size check runs before hashing, so a truncated four-gigabyte transfer
fails in milliseconds rather than after a full digest. It only runs when the
record declares a `size`, which is one reason to declare one.

Where some mirrors ended short and others could not be reached at all, the
broader `getaca_error_unavailable` is raised instead, with each mirror's
reason listed. Retrying helps one kind of failure and not the other, so
claiming the whole thing was a truncation would be wrong.

## `getaca_error_credentials`

Every mirror that failed answered 401 or 403. The resource is reachable and
the machine has a network; what is missing is permission. Telling that user to
connect to a network would send them looking in the wrong place, which is the
same reasoning that separates a truncation from an outage.

```{r, eval = FALSE}
getaca("gedi-l4a", package = "canopy")
#> Error: Access to canopy/gedi-l4a@2026-06 was refused.
#>   data.ornldaac.earthdata.nasa.gov: bearer credential from EARTHDATA_TOKEN (not set: EARTHDATA_TOKEN)
#>     obtain one at https://urs.earthdata.nasa.gov/users/new
#>
#> Actions: set the variables named above and retry; or, if a credential is
#> already set, check that it is current and carries access to this resource.
#>
#> Fix: see the actions listed above.
```

Which variable is wanted comes from the declaration rather than from the
response, because a server that never saw a credential and one that rejected
the credential it saw answer alike. The message says which of the two happened,
since "not set" and "set, and refused" call for different next steps.

`err$demands` carries one entry per host, each with `host`, `scheme`,
`variables`, `missing` and `register`. Nothing in the condition holds a
credential; the value is read at the moment of the request and never stored.

This is the one condition whose `actor` depends on the situation. Where the
declaration names a credential, the person who can act is the user holding it.
Where a host refuses and the declaration names none, there is no variable to
set: either the resource became restricted after it was declared or the
location is wrong, and both are the declaring package's to fix, so `actor` is
`"author"` and the message says so instead of asking for a variable that does
not exist.

To ask before fetching rather than after failing, `getaca_credentials()`
reports every variable a package's declaration reads and whether it is set:

```{r, eval = FALSE}
getaca_credentials(package = "canopy")
#>   package                             host scheme        variable   set
#> 1  canopy data.ornldaac.earthdata.nasa.gov bearer EARTHDATA_TOKEN FALSE
#>                                   register
#> 1 https://urs.earthdata.nasa.gov/users/new
```

See `vignette("declaring")` for how a package declares one.

## `getaca_error_upstream_changed`

A complete transfer whose digest does not match the declaration. The
publisher appears to have replaced the file without issuing a new version.

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: The remote file no longer matches yourpkg/backbone@2026-06.
#>   declared SHA-256: 9f9f9f...
#>   observed SHA-256: 3c1a77...
#>   source: https://primary.invalid/backbone-2026-06.zip
#>
#> The publisher appears to have replaced the contents without issuing a
#> new version. getaca will not accept the substitution, and any copy
#> already verified in the cache has been left untouched.
#>
#> Fix: the publisher changed the file. The declaring package needs a new
#> registry entry.
```

Two properties of this failure are the point of the package. The new bytes are
not accepted, so nothing downstream silently starts computing on a different
dataset. And a copy already in the cache is untouched, so a machine that has
the correct file keeps working while the situation is sorted out.

As a user, there is nothing to do here except report it. As the declaring
author, the response is to work out which of four things happened: a corrupted
transfer that will not reproduce, a genuine upstream recut that deserves a new
version record, an upstream mistake worth reporting to the publisher, or a
wrong checksum in your own registry.

## `getaca_error_declaration`

Several independent mirrors returned identical bytes, and none matched the
declared checksum. When every mirror agrees with the others and disagrees with
the registry, the registry is the likely error.

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: The declared checksum for yourpkg/backbone@2026-06 looks wrong.
#>   2 independent sources agreed on SHA-256 3c1a77...
#>   the registry declares 9f9f9f...
#>
#> When every mirror agrees with the others and disagrees with the
#> registry, the registry is the likely error.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.
```

This is the condition that distinguishes an author's mistake from a
publisher's. One mirror disagreeing is an upstream mutation; two mirrors
agreeing with each other and not with you is a typo, a checksum computed
before an upload recompressed the file, or a record copied from the wrong
row.

It requires more than one mirror to be diagnosable at all, which is a
practical argument for declaring two.

## `getaca_error_composition`

Every declared `part()` arrived and matched its own checksum, and combining
them produced something other than the artefact the record names.

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: The parts declared for yourpkg/backbone@2026-09 do not produce the declared bytes.
#>   3 parts, each matching its own checksum, combined by 'concat'
#>   declared SHA-256: b1b1b1...
#>   composed SHA-256: 2c40f9...
#>
#> Every part arrived intact, so this is not a transfer problem. Either the
#> series is not the one this version is made of, or it is put together by
#> something other than what the registry names.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.
```

This is `getaca_error_declaration` one level up. There, the transport was sound
and the checksum was wrong; here every checksum in the series was right and what
they add up to was wrong. A part that goes missing or arrives changed raises the
ordinary transfer conditions instead, naming which part of the series it was:

```{r, eval = FALSE}
#> Error: Cannot reach any source for yourpkg/backbone@2026-09 (part 2 of 3).
```

As an author, reach for this after re-splitting a file, reordering a series, or
changing what a `combiner()` does without changing its id. Nothing reaches the
cache, so a user hitting it is not left with a half-composed artefact.

## `getaca_error_cache_corrupt`

The cached copy no longer matches its own entry record. Raised by the cheap
size check on ordinary access, and by the scheduled or forced re-hash.

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: The cached copy of yourpkg/backbone@2026-06 is damaged.
#>   path: ~/.cache/R/getaca/yourpkg/backbone/2026-06/raw/backbone-2026-06.zip
#>   declared SHA-256: 9f9f9f...
#>   observed SHA-256: 71b0c2...
#>
#> Action: getaca_clean("backbone", package = "yourpkg"), then retry.
#>
#> Fix: see the actions listed above.
```

Silent repair was considered and rejected. Refetching without saying anything
would hide a disk developing bad sectors, a sync client rewriting files, and a
colleague who opened something in the cache directory and saved it. The
message names the clean-up call, so the repair is one line and the user knows
it happened.

```{r, eval = FALSE}
tryCatch(
  getaca("backbone", package = "yourpkg"),
  getaca_error_cache_corrupt = function(e) {
    warning("The cached backbone was damaged; refetching.", call. = FALSE)
    getaca_clean("backbone", package = "yourpkg")
    getaca("backbone", package = "yourpkg")
  }
)
```

That handler is reasonable in a package that knows its users would want the
repair. Doing it inside `getaca` for everyone would make the failure
invisible.

## `getaca_error_redeclared`

The cached copy still matches its own record, and the declaration in force has
moved to different bytes for that version. Both are intact, which is what
separates this from a corrupt cache:

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: The declaration for yourpkg/backbone@2026-06 no longer names the bytes already held.
#>   cached SHA-256:   9f9f9f...
#>   declared SHA-256: 71b0c2...
#>
#> A version identifies exact bytes, so these cannot both be that version.
#> The cached copy was verified against the earlier declaration and has
#> been left untouched. Publish a new version instead of redefining this
#> one.
#>
#> Action: if the new declaration is the correct one, drop the old copy
#> with getaca_clean("backbone", package = "yourpkg") and retry.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.
```

Resolving quietly to the copy already held would be the wrong answer: it makes
one machine keep bytes every other machine has stopped using, and the
disagreement stays invisible for as long as that cache survives. `verify = TRUE`
asks the same question, so a forced re-hash cannot confirm bytes against a
declaration they no longer match either.

A version reaches this state when a package upgrade brings an edited record for
a version this machine has already fetched. A remote registry or a pin file
trying the same thing is stopped earlier, at
`getaca_error_invalid_registry`, since neither is allowed to redefine a
published version at all.

## `getaca_error_invalid_registry`

The declaration is malformed or internally inconsistent. Raised at
`registry()` and `resource()` where possible, which puts it on the author's
machine rather than a user's:

```{r, error = TRUE}
registry("yourpkg", list(
  resource("backbone", "2026-09",
           urls = "https://host.invalid/a",
           sha256 = strrep("ab", 32)),
  resource("backbone", "2026-03",
           urls = "https://host.invalid/b",
           sha256 = strrep("cd", 32))
))
```

Every problem found is listed, rather than the first one:

```{r, error = TRUE}
resource("backbone", "2026-06",
         urls = "ftp://host.invalid/a", sha256 = "abc")
```

The same condition covers the runtime cases: a package that ships no registry,
a name the registry does not declare, a remote registry declaring a different
package, and a pin file that redefines a published version.

```{r, error = TRUE}
resolve_resource("no-such-resource", registry = reg)
```

The message names what is on offer, which is usually enough to spot a typo
without opening the registry.

```{r, error = TRUE}
resolve_resource("backbone", package = "stats")
```

That one names the conventional path, so a user who expected a package to
declare resources can see immediately that it does not.

## `getaca_error_signature`

A remote registry arriving without a signature this package's own keys accept.
Raised only where the bundled registry declares `keys`; a package declaring none
never fetches a signature and never meets this:

```{r, eval = FALSE}
getaca("backbone", package = "yourpkg")
#> Error: The registry for 'yourpkg' could not be established as authentic.
#>   no signature could be fetched and read
#>   source: https://yourpkg.invalid/getaca-registry.rds.sig
#>
#> This package declares signing keys, so a remote registry that cannot be
#> checked against one is refused rather than used. The bundled declaration
#> the package ships is unaffected and still resolves.
#>
#> Action: getaca_policy("bundled") resolves through it for this session.
#>
#> Fix: the declaring package needs a correction. Report it to its maintainer.
```

The same condition covers a signature from a key the package does not declare,
one whose expiry has passed, and one covering a declaration state older than the
installed registry, which is what stops an old genuine registry being replayed
in place of the current one.

A host that cannot be reached is a different situation and gets the other
answer: resolution falls back to the bundled declaration with a message. A
registry that arrives and fails verification stops instead. A registry arriving
without its signature is the second, since one host serves both, and treating a
missing signature as an outage would let whoever serves the file decide whether
the check runs at all.

See `vignette("policies")` for how a package declares keys and signs what it
publishes.

## Writing a handler in a declaring package

The pattern that works is to catch the user-actionable conditions and speak
your own domain, and to let the author-actionable and upstream-actionable ones
through unchanged. A user cannot act on either, and the default messages
already say who can.

```{r, eval = FALSE}
install_backbone <- function(name = "backbone") {
  tryCatch(
    open_backbone(getaca(name, package = "yourpkg")),

    getaca_error_offline = function(e) {
      stop("The ", name, " backbone is not installed and this session cannot ",
           "download it.\n",
           "On a connected machine run:\n",
           "  yourpkg::install_backbone(\"", name, "\")\n",
           "or point GETACA_CACHE at a directory that already holds it.",
           call. = FALSE)
    },

    getaca_error_unavailable = function(e) {
      stop("The ", name, " backbone could not be downloaded from any of its ",
           length(e$urls), " sources.\n",
           "This is usually temporary. Try again, or use a smaller reference:\n",
           "  yourpkg::analyse(x, backbone = \"grid\")", call. = FALSE)
    },

    getaca_error_cache_corrupt = function(e) {
      stop("The cached ", name, " backbone is damaged.\n",
           "Repair it with:\n",
           "  getaca::getaca_clean(\"", name, "\", package = \"yourpkg\")",
           call. = FALSE)
    }
  )
}
```

Three things this gets right. It names your function rather than `getaca()`,
so the user's next step is in your vocabulary. It suggests a domain-level
alternative for the transient case. And it leaves
`getaca_error_upstream_changed` and `getaca_error_declaration` alone, which
means a user who hits one gets a message that tells them to report it, and a
report that contains the checksums you need.

## Testing your handlers

Conditions can be constructed by making a resolution fail, which needs no
network:

```{r}
offline_error <- tryCatch(
  getaca("backbone", registry = reg, policy = "offline"),
  getaca_error = function(e) e
)
class(offline_error)[1]
```

```{r, eval = FALSE}
test_that("a missing backbone gets a yourpkg-flavoured message", {
  withr::local_envvar(c(GETACA_OFFLINE = "true"))
  withr::local_options(list(getaca.cache = withr::local_tempdir()))

  expect_error(install_backbone("backbone"), "On a connected machine run")
})
```

Forcing `GETACA_OFFLINE` with an empty cache is the cheapest way to exercise
the path a user without the resource takes, and it is deterministic on every
machine.

For the conditions that need a transfer, the honest test is against a real
resource on CI with `NOT_CRAN=true`, guarded by
`getaca_skip_if_unavailable()`. See `vignette("checks")`.

## Where to go next

- `vignette("checks")` for behaving during checks and in CI

- `vignette("cache")` for what `getaca_error_cache_corrupt` is protecting

- `vignette("declaring")` for the two conditions that name you

```{r, include = FALSE}
options(.old_options)
Sys.unsetenv(names(.old_envvars)[is.na(.old_envvars)])
.restore <- .old_envvars[!is.na(.old_envvars)]
if (length(.restore)) do.call(Sys.setenv, as.list(.restore))
```
