Preparation for a first CRAN submission. Everything here came out of reading the package the way a CRAN reviewer reads one, which is a different pass from the one that gets a suite green.
tools::R_user_dir("biohttp", "cache") rather than
data/cache. The old default was a relative path, so it
resolved against whatever directory R was started in: a job run from two
working directories quietly kept two caches, and neither knew about the
other. CRAN policy also asks that a package write outside
tempdir() only where a user expects it to, and a directory
appearing under the working directory is not that place. Only a caller
who had opted into the disk tier with BIOHTTP_CACHE_DISK is
affected, and setting BIOHTTP_CACHE_DIR still overrides it.
An existing data/cache is not migrated; it is a cache, so
the cost of abandoning it is one cold start.tools joins Imports for that call. It
ships with R, so it adds nothing to what a consumer installs, and it is
declared because R CMD check requires a declaration for
anything reached with ::.DESCRIPTION gains a reference for the circuit breaker,
names the language as en-US, and hyphenates
“service-specific”.Every environment variable the package reads now treats an empty
export the same way it treats an absent one. Sys.getenv()’s
default argument only fires when a name is absent, so a container
passing through a variable its operator never filled in got
"" rather than the default, and four readers were doing
that. env_num() and env_flag() had always had
the rule; env_chr() gives it to the rest.
The one that mattered was BIOHTTP_CALLER_IDENTITY.
Exported empty, it sent User-Agent: biohttp/0.1.2 out as
/0.1.2: a version with nothing in front of it, which is the
one thing a user agent exists to carry, on a package whose point is
being attributable to the service you are calling. Every test in the
suite ran that way, because setup.R clears those variables
by exporting them empty, and no test looked.
BIOHTTP_CACHE_DIR was the same defect with a smaller
blast radius: "" as a directory is the working directory,
which is what the change above removes.
BIOHTTP_CONTACT_URL, BIOHTTP_CONTACT_EMAIL and
BIOHTTP_CACHE_SALT all default to "" anyway,
so they read the same as before.
The status_message() example no longer calls
withr, which is in Suggests. An example may
only use what Imports guarantees: CRAN checks with
_R_CHECK_DEPENDS_ONLY_ set, where a Suggests
package is simply absent and the example errors. It now sets the option
with options() and restores it, which also satisfies the
separate rule that an example puts back anything it changes.
inst/CITATION is shipped, so
citation("biohttp") carries the Zenodo DOI from an
installed copy. CITATION.cff is in
.Rbuildignore and never reached a tarball, so the README
had been promising something only a source checkout could deliver. Both
exist now, because GitHub’s cite box reads the .cff and R
reads inst/CITATION.CONTRIBUTING.md and
LICENSE.md by relative path. Both are in
.Rbuildignore, so neither reaches a tarball and both links
were dead for anyone reading the README somewhere other than GitHub,
which from here includes the CRAN package page. They are absolute
now.Imports list named five packages where
DESCRIPTION names six. A README that contradicts the
DESCRIPTION printed beside it on the CRAN page is worth
more than the line it saves.NEWS.md
already answers the question.Everything here came out of the first real migration onto 0.1.0, which is where a transport contract meets an app that already had its own opinions.
cache_reset() no longer orphans a reference taken from
cache(). It used to drop the store, so the next
cache() built a new one and anything held earlier pointed
at a dead object: writes went where nothing else could see them, reads
returned stale entries, and the only symptom was a hit rate quietly
falling to zero. The reset now clears the store in place when no cache
setting has changed, and only rebuilds when one has, which is what test
isolation needs (#21).BIOHTTP_CACHE_MAX_N bounds the memory tier by number of
entries. BIOHTTP_CACHE_MAX_SIZE bounds bytes, and a
long-running process answering many small responses stays well under
that while holding far more entries than intended. Defaults to
cachem’s own Inf, so nothing changes unless it
is set (#22).status_message() produces every user-facing sentence in
the package, and setting the biohttp.status_message option
replaces them. That is how an app keeps its own voice, or localizes,
while adopting the transport. Returning anything other than a single
non-empty string falls back to the built-in, so overriding one status
and leaving the rest is the expected use. The override runs inside
tryCatch(), because it sits on the failure path and the
package promises never to raise there (#23).status_*() constructors used to hardcode their own
copies of sentences that also lived in
http_error_message(), and the two had drifted: three
different sentences existed for “temporarily unavailable”. They now
share one set. status_error()’s default gains “Please try
again.” as a result, which is the only user-visible wording change.classify_http() has always called 408
a timeout, so the envelope said timeout while the sentence
a user read said something else. They agree now.BIOHTTP_CACHE_MAX_SIZE and
BIOHTTP_CACHE_DISK_TTL, which it had never listed.DESCRIPTION, .zenodo.json
and CITATION.cff agree on the version. Nothing read it from
one source, so a stale archive label was a matter of time.First version with a public contract. The envelope shape is fixed from here; changing it after this is a breaking change.
envelope() and never raises on an
HTTP or a parse failure. Callers branch on res$status and
write no tryCatch() of their own.ok,
no_data, stale, rate_limited,
timeout, skipped, error.
ok is derived from status so the two cannot
disagree.error carries one sentence fit for a user;
detail carries the technical cause for a log. They are
never mixed.vignette("biohttp") documents the whole thing.get_json(), post_json(), and
get_text() assemble, perform, and cache a call.
perform() and perform_text() take a request
you built yourself.req_defaults() applies the timeout, bounded retry on
transient codes, an attributable user agent, optional throttling, and
redacted headers. A token never prints in an inspected request, a log
line, or an error message.perform_many() performs a list of prepared requests and
returns a list of envelopes the same length and in the same
order, so a caller zips results back onto its inputs by
position.get_json_many() and post_json_many() serve
whatever the cache already holds, perform only the entries it is
missing, and store the successes. A warm gene list becomes zero
requests, and a half-warm one becomes only as many as are genuinely
unknown.req_throttle() and req_retry() across
a whole list rather than per request, so requests are grouped by host
and the groups run one after another. Fanning out across a dozen
different services needs process-level concurrency, which belongs in an
application rather than in a transport package.skipped. Inside a
batch the breaker cannot short-circuit requests already in flight, so
transport failures recorded there take effect on the next call.secret_query, a named list of query
parameters carrying a credential. NCBI E-utilities is why it exists: its
api_key raises a caller from 3 to 10 requests a second and
there is no header form, so the credential has to travel in the
URL.req$url beforehand, the cache key included, can carry
it.headers, which is part of the key.
SECURITY.md states the rule.redact_secrets() is applied to the messages built from
a transport or a parse failure, because a curl error normally carries
the URL that failed. It matches both the raw value and the
percent-encoded form a URL carries, so a key holding +,
/, or = is caught.httr2::req_url_query() has no .redact, and
passing one does not error: it is taken as another query parameter and
appended to the URL. This package therefore does the redaction itself
rather than delegating it.as_legacy_envelope() translates to the older four-field
shape so an existing app can migrate in a reviewable diff. Deprecated on
arrival, scheduled for removal in 0.3.0.