Help us shape the future of Go! Take the 2026 Go Developer Survey

pluginapi

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

Kryspal Media Plugin SDK

Kryspal Media Plugin protocol v1: a plugin is a standalone executable that the server starts as a child process and talks to with gRPC over a Unix socket.

Path Contents
PROTOCOL.md Protocol specification: handshake, call order, health checks, configuration, capabilities, errors, plugin catalog and packages
handshake.go (package pluginapi) Handshake environment variables, cookie, handshake line format and parsing, protocol version negotiation; shared by the host and the SDK
v1/*.proto Package kryspal.plugin.v1. Plugin side: Plugin (Describe, Configure), MetadataProvider (Search, GetItem, GetPerson, SearchPerson, GetCollection), ImageProvider, SubtitleProvider, UserSync, Recognizer, SegmentProvider, DanmakuProvider, PersonAliasProvider, CastTarget. Host side: Host, served by the host to plugins
v1/*.pb.go (package pluginv1) Generated with gen.sh
v1/protocol.go Protocol constants: capabilities, health check service names, reading and writing the retry-after-seconds trailer
i18n/ Plugin localization helpers: string tables, language fallback chain (zh-CN → zh-TW → en)
sdk/ SDK for Go plugin authors: sdk.Main(sdk.Config{Plugin: p, MetadataProvider: p})
plugintest/ Conformance suite: runs a plugin binary the way the host does and checks the protocol contract
examples/minimal/ The quickstart plugin: one capability, no configuration
examples/people/ People and recognition: GetPerson with a gallery and attributes, SearchPerson confirmed by works, person images, a Recognizer for catalog codes, host features
examples/static/ Example plugin that returns fixed data; the tests compile it on the spot and start it the way the host does
gen.sh Regenerates v1/*.pb.go with pinned generator versions

Quickstart

The SDK is published as the Go module github.com/kryspal/plugin-sdk (Apache-2.0). Releases are v0.x until the API is frozen. Outside this repository, the import paths are github.com/kryspal/plugin-sdk/sdk and github.com/kryspal/plugin-sdk/pluginv1.

  1. Create a module and add the SDK:

    mkdir minimal && cd minimal
    go mod init example.com/minimal
    go get github.com/kryspal/plugin-sdk@latest
    
  2. Copy examples/minimal/main.go: a movie metadata provider that declares one capability (metadata.movie), needs no configuration and knows one title.

  3. Build a static binary: CGO_ENABLED=0 go build -trimpath -o minimal .

  4. Check it with the conformance suite (below).

  5. Try it on your own server: start the server with MEDIASERVER_DEV_PLUGINS pointing at a directory of builds (PROTOCOL.md §9.5); no catalog or signature is needed.

  6. Package it with a catalog entry (PROTOCOL.md §9) and add the catalog to the server.

Conformance suite

plugintest.RunSource builds your plugin's main package and runs it the way the server does; plugintest.Run does the same with a binary you built yourself. Both take the path of a main package or a binary, never a source file. The suite checks:

  • refusing to run without the cookie or a shared protocol version, the handshake line and socket location;
  • the health check, Describe (required fields, only text changes with the language) and Configure with your test configuration;
  • with Options.Metadata: Search, then GetItem on the first candidate; item images must be HTTPS with a known type, and attributes must follow the rules in PROTOCOL.md §7; an ID of the entry that is kept under a key other than the plugin ID is reported, for candidates, the item and its credits;
  • with images.movie or images.series declared: GetImages with the item's provider IDs;
  • with metadata.person declared: GetPerson (for Options.Person, or the first credit of the item), whose provider_ids must hold the person's own ID under the plugin ID, with person images of type IMAGE_TYPE_PRIMARY, IMAGE_TYPE_PHOTO or IMAGE_TYPE_BACKDROP, and GetImages for the person when images.person is declared;
  • with Options.PersonSearch (metadata.person.search): SearchPerson candidates have an ID, a name and their own ID under the plugin ID;
  • with Options.Subtitles (subtitles.search): the subtitle Search answer has unique IDs, a language, a lowercase format, a matched_by, scores and ratings in range, a short comment, and keeps to the limit; a field autofill_daily_limit in the manifest is an int with a default of 0 to 1000, only on subtitle providers and only when the host lists autofill_limit;
  • with media.recognize declared: an empty path returns no candidates and no error; with Options.Recognize, a known path is recognized, the answer is the same twice, at most three candidates, and parser_version is set;
  • shutdown on SIGTERM.
func TestConformance(t *testing.T) {
	plugintest.RunSource(t, ".", plugintest.Options{
		Metadata: &plugintest.MetadataCase{Type: pluginv1.ItemType_ITEM_TYPE_MOVIE, Name: "Big Buck Bunny", Year: 2008},
	})
}

Point the plugin at a fake upstream with Options.Setup (runs before the plugin starts) and Options.Env (extra environment variables; the host's own variables cannot be replaced):

plugintest.RunSource(t, ".", plugintest.Options{
	Setup: func(t *testing.T) map[string]string {
		srv := httptest.NewTLSServer(fakeUpstream())
		t.Cleanup(srv.Close)
		return map[string]string{"MYPLUGIN_BASE_URL": srv.URL}
	},
	Metadata: &plugintest.MetadataCase{Type: pluginv1.ItemType_ITEM_TYPE_MOVIE, Name: "Sample Movie"},
})

plugintest.NewUpstream is a scripted fake service that records the requests it gets. A plugin that only accepts an https base URL needs plugintest.NewTLSUpstream, whose self-signed certificate the plugin trusts through SSL_CERT_FILE. Go reads that variable only on Linux and other Unix systems, so on macOS and Windows the test is skipped with a message; run it on Linux (for example in CI):

up := plugintest.NewTLSUpstream(t)
up.Handle("GET /subtitles", plugintest.JSON(200, answer))
plugintest.Run(t, bin, plugintest.Options{
	Config:    map[string]string{"base_url": up.URL},
	Env:       up.Env(),
	Subtitles: &plugintest.SubtitleCase{Type: pluginv1.ItemType_ITEM_TYPE_MOVIE, Title: "Sample Movie", Year: 2020},
})

For anything the suite does not cover (user sync, segments, danmaku, alias maps, cast targets, your own edge cases), plugintest.Start starts the plugin the same way and returns a gRPC connection to it; the plugin is stopped when the test ends:

conn := plugintest.Start(t, plugintest.Build(t, "."), plugintest.Options{})
resp, err := pluginv1.NewSubtitleProviderClient(conn).Search(t.Context(), req)

The plugin cannot be run by hand: it exits with "this is a Kryspal Media plugin" unless the server starts it (§2).

For Go plugin authors

type provider struct {
	pluginv1.UnimplementedPluginServer
	pluginv1.UnimplementedMetadataProviderServer
}

// Implement Describe, Configure, and the MetadataProvider methods for the capabilities you declare.

func main() {
	p := &provider{}
	sdk.Main(sdk.Config{Plugin: p, MetadataProvider: p})
}
  • Logs go to stderr; nothing may be written to stdout after the handshake (sdk.Main points os.Stdout at stderr).

  • Errors: sdk.NotFound, sdk.InvalidArgument, sdk.NeedsConfiguration, sdk.Unavailable, sdk.RateLimited(retryAfter, msg). A panic in a handler becomes INTERNAL and does not crash the plugin.

  • Localization: Describe and Configure requests carry a language, and all display text must be returned in that language (rules in PROTOCOL.md §5.1). Use pluginapi/i18n:

    var messages = i18n.New(map[string]map[string]string{
    	"en":    {"desc": "Example provider.", "required": "Required."},
    	"zh-TW": {"desc": "…", "required": "…"}, // Traditional Chinese text
    	"zh-CN": {"desc": "…", "required": "…"}, // Simplified Chinese text
    })
    
    func (p *provider) Describe(_ context.Context, req *pluginv1.DescribeRequest) (*pluginv1.Manifest, error) {
    	t := messages.For(req.GetLanguage()) // zh-CN → zh-TW → en; ja, the empty string, etc. → en
    	return &pluginv1.Manifest{Description: t.T("desc") /* … */}, nil
    }
    

    When T cannot find a key it returns the key itself, so a missing translation is obvious at a glance; every key needs at least en. See examples/static for a complete example and examples/static/i18n_test.go for a test example (English contains no Han characters; Simplified and Traditional Chinese are not mixed).

  • Versions: releases are v0.x until the API is frozen; v1.0.0 follows the freeze.

  • Images, subtitles, user sync: the ImageProvider, SubtitleProvider and UserSync fields of sdk.Config (required when the matching capability is declared). Error helpers: sdk.Unauthenticated (the credential is no longer valid), sdk.RateLimitedCredential (rate-limits only this credential); examples/static/services.go demonstrates all three services. Plugins are stateless: the host passes the credential in on every call, and updates come back with the response.

  • Two Search methods: the metadata Search and the subtitle Search have the same name, so one Go type cannot serve both. Implement MetadataProviderServer and SubtitleProviderServer on two separate types and pass them in sdk.Config.MetadataProvider and sdk.Config.SubtitleProvider.

  • Subtitle limits: the scheduled automatic-download task makes at most 1000 searches per run, 3 seconds apart (across all libraries), and downloads at most autofill_daily_limit subtitles per day from each plugin (default 10, 0 means no limit; PROTOCOL §7.2). A plugin whose source has no download quota declares an int field with the key autofill_daily_limit and the default "0", and only when the host lists the feature autofill_limit.

Licensing for plugin authors

The SDK is licensed under the Apache License 2.0 (see LICENSE; trademark rules in TRADEMARKS.md). You may use it in open-source and closed-source plugins. A plugin binary built with the SDK links these components, and distributing the binary carries their obligations:

Component License When you distribute a binary
This SDK (including the OpenCC data in hanconv/, if you import it) Apache-2.0 Include the license text and reproduce NOTICE (and hanconv/NOTICE if you use hanconv)
google.golang.org/grpc Apache-2.0, with a NOTICE.txt Include the license text and reproduce its NOTICE.txt
google.golang.org/genproto/googleapis/rpc Apache-2.0 Include the license text
google.golang.org/protobuf BSD-3-Clause (plus a patent grant) Reproduce the copyright notice and license terms in your documentation
golang.org/x/net, golang.org/x/sys, golang.org/x/text BSD-3-Clause (plus a patent grant) Same as above

None of the SDK's dependencies is under a copyleft license. The exact list depends on what your plugin imports; a tool such as go-licenses can produce it for your binary:

go run github.com/google/go-licenses@v1.6.0 report ./... > THIRD_PARTY_LICENSES.csv
go run github.com/google/go-licenses@v1.6.0 save ./... --save_path=third_party_licenses

More things to keep in mind:

  • To be listed in the official plugin catalog, a plugin must be open source under an OSI-approved license. Plugins that are not listed may use any license and are installed by users from catalogs they add themselves.
  • If you choose a GPL license, prefer GPL-3.0 or "GPL-2.0-or-later": the Apache License 2.0 is not compatible with GPL-2.0-only.
  • Complying with the terms of the services your plugin calls (attribution, caching limits, commercial use) is your responsibility. Use the Attribution field of the manifest for the credits a service requires.
  • An application key compiled into a binary (for example with -ldflags -X) is not secret: anyone can extract it. Check whether the service's terms allow that, and offer a user-supplied key as a secret configuration field.
  • Never write keys or tokens to logs or error messages.

Tests

go test ./...   # from this directory

The examples/static tests go build the example plugin into t.TempDir(), start it in a clean environment, perform the handshake, and call Describe, the health check, Configure, Search, GetItem, GetPerson and GetCollection over the Unix socket; they verify FAILED_PRECONDITION and the retry-after trailer of RESOURCE_EXHAUSTED, and finally send SIGTERM to confirm a graceful shutdown that removes the socket.

Regenerating

PROTOC=/path/to/protoc ./gen.sh   # from this directory; protoc-gen-go and protoc-gen-go-grpc must be on PATH (protoc 36.2, protoc-gen-go v1.36.12, protoc-gen-go-grpc 1.6.2)

Documentation

Overview

Package pluginapi defines the constants and handshake line format of the Kryspal Media plugin handshake, shared by the host and the SDK.

See PROTOCOL.md for the full specification. The gRPC services are defined in the subpackage v1 (pluginv1); Go plugin authors use the subpackage sdk. This package depends only on the standard library.

Index

Constants

View Source
const (
	// EnvCookie must equal CookieValue; otherwise the plugin should refuse to start, because it was not started by the host.
	EnvCookie = "KRYSPAL_PLUGIN_COOKIE"
	// EnvProtocols holds the protocol major versions the host supports, as comma-separated decimal numbers, e.g. "1" or "1,2".
	EnvProtocols = "KRYSPAL_PLUGIN_PROTOCOLS"
	// EnvSocketDir is the 0700 directory the host created for this plugin; the plugin creates its Unix socket in it.
	EnvSocketDir = "KRYSPAL_PLUGIN_SOCKET_DIR"
	// EnvDataDir is the plugin's own data directory (the same path as HOME): keep caches and other files here. It is
	// kept across upgrades and removed on uninstall. Hosts before SDK v0.2.0 do not set it; use DataDir, which falls
	// back to HOME.
	EnvDataDir = "KRYSPAL_PLUGIN_DATA_DIR"
	// EnvHostFeatures lists, comma-separated, the compatible protocol additions the host stores and uses (the
	// HostFeature* values). Older hosts do not set it; see HostFeatures.
	EnvHostFeatures = "KRYSPAL_HOST_FEATURES"
	// EnvHostSocket and EnvHostToken are the Unix socket path and one-time token of the service the host offers to plugins
	// (Host). They differ on every process start; older hosts do not set them, in which case the SDK's Host()
	// returns ErrHostUnavailable and the plugin should degrade gracefully.
	EnvHostSocket = "KRYSPAL_HOST_SOCKET"
	EnvHostToken  = "KRYSPAL_HOST_TOKEN"
)

Environment variables for the handshake. The host starts a plugin with a clean environment, passing only these three variables plus PATH, TMPDIR and HOME, and SSL_CERT_FILE and SSL_CERT_DIR when the host has them set.

View Source
const (
	NetworkUnix = "unix"
	// NetworkTCP is reserved for future plugins that run over the network; the first host version does not accept it.
	NetworkTCP = "tcp"
)

Network types in the handshake line.

View Source
const (
	HostFeatureAttributes = "attributes" // ItemMetadata.attributes and PersonMetadata.attributes
	HostFeaturePhoto      = "photo"      // IMAGE_TYPE_PHOTO for people
	// HostFeatureStill: IMAGE_TYPE_STILL for items. Without it the host skips STILL images, so a plugin sends its stills
	// as BACKDROP images after the main backdrop instead.
	HostFeatureStill = "still"
	// HostFeatureConfigTypes: the setting types text, string_list and ordered_multiselect, and ConfigField.visible_when.
	HostFeatureConfigTypes = "config_types"
	// HostFeatureImageReferer: Manifest.image_fetch_rules.
	HostFeatureImageReferer = "image_referer"
	// HostFeatureTasks: the host schedules and runs Manifest.tasks (capability plugin.tasks). Without it, a plugin that
	// needs periodic work runs its own timer.
	HostFeatureTasks = "tasks"
	// HostFeatureImageCrop: Image.crop, Image.focus and Image.crop_hint. Without it the host uses the whole image, so a
	// plugin that needs a crop (for example a portrait poster cut from a wide cover) should send an uncropped image only
	// where it fits.
	HostFeatureImageCrop = "image_crop"
	// HostFeatureConfigItems: ConfigGroup.items and ConfigField.item. Without it the host shows an item's fields as
	// ordinary fields of the group, so a plugin with many items may fall back to a select plus visible_when.
	HostFeatureConfigItems = "config_items"
	// HostFeatureItemChecks: the host shows a per_item check (§7.9) next to every entry of the group's list and calls
	// RunCheck with RunCheckRequest.item. Without it the host runs the check once for the whole group with an empty
	// item, so a plugin should then test every entry the user configured.
	HostFeatureItemChecks = "item_checks"
	// HostFeatureScrapeNotices: the host shows Notice entries from metadata and image results to the user and fetches
	// again after retry_after_seconds when a result says incomplete. Without it notices are ignored and
	// an incomplete result is final.
	HostFeatureScrapeNotices = "scrape_notices"
	// HostFeatureFieldSources: the host stores ItemMetadata.field_sources, PersonMetadata.field_sources and
	// Image.source next to each field's provenance and shows them (§7.10). Without it they are ignored; sending them is
	// harmless.
	HostFeatureFieldSources = "field_sources"
	// HostFeaturePersonBackdrop: the host stores a person's IMAGE_TYPE_BACKDROP (PersonMetadata.images and
	// GetImages with ITEM_TYPE_PERSON) as the person's backdrop and only fills a missing one. Older hosts take every
	// person image as a photo candidate, so send person backdrops only when the host declares this.
	// Automatic fills use only the backdrops in PersonMetadata.images (what the metadata source thinks fits); the host
	// does not fill person backdrops from GetImages with ITEM_TYPE_PERSON and IMAGE_TYPE_BACKDROP.
	HostFeaturePersonBackdrop = "person_backdrop"
	// HostFeatureAutoFillLimit: the host stores and uses the setting key autofill_daily_limit (a plugin's daily limit
	// for automatic subtitle downloads, 0 means unlimited). Declare the key, only to give it a default, when the host
	// declares this; on older hosts it would be a setting with no effect.
	HostFeatureAutoFillLimit = "autofill_limit"
	// HostFeatureSearchConfirmed: SearchCandidate.confirmed. The host applies a single confirmed candidate
	// automatically whatever its score; several confirmed candidates are ambiguous and left for the user to choose.
	// Without it the host judges candidates by its own scoring only, so a plugin still sends a score.
	HostFeatureSearchConfirmed = "search_confirmed"
	// HostFeatureBadgeSuggestions: Manifest.suggested_badge_rules. The host lists them on its badge settings for the
	// administrator to add; it never enables them by itself. Without it they are ignored.
	HostFeatureBadgeSuggestions = "badge_suggestions"
	// HostFeatureConfirmedNumbering: Recognition.numbering_confirmed. The host takes a confirmed season,
	// episode and episode_end as an episode's numbering in place of its own parse; a season folder still decides the
	// season of the files inside it, and NFO numbers still come first. Without it the field is ignored.
	HostFeatureConfirmedNumbering = "confirmed_numbering"
	// HostFeatureLibraryConfig: ConfigField.library_override, LibraryContext in requests, and Configure with dry_run
	// for per-library settings. Without it requests carry no LibraryContext and plugins use their global settings only.
	HostFeatureLibraryConfig = "library_config"
	// HostFeatureRecognitionHint: SearchRequest.recognition and GetItemRequest.recognition. The host also writes the
	// provider IDs a recognition plugin found into the item's provider IDs (not over manual or NFO ones). Without it
	// recognition results reach metadata plugins only as titles and known IDs.
	HostFeatureRecognitionHint = "recognition_hint"
	// HostFeatureLookupNames: GetDanmakuRequest and SubtitleTarget carry original_title and product_code. Without it
	// both are empty, and a plugin looks up by title, year, provider IDs and season and episode only.
	HostFeatureLookupNames = "lookup_names"
)

Values of EnvHostFeatures. Each names a compatible addition to protocol v1 that an older host ignores, so a plugin can tell whether the host will keep what it sends (for example, to put the same facts into the overview for hosts that drop attributes).

View Source
const CookieValue = "7f3a9c52-kryspal-plugin"

CookieValue is the fixed magic value of EnvCookie. It is not a secret; it only keeps users from running a plugin directly.

View Source
const MaxHandshakeLine = 1024

MaxHandshakeLine is the maximum length in bytes of the handshake line (excluding the newline). The host treats a longer line as a failed handshake.

View Source
const SocketName = "plugin.sock"

SocketName is the socket file name the SDK creates in EnvSocketDir. Plugins in other languages may choose their own name, but the full path must not exceed the Unix socket limit (107 bytes on Linux).

Variables

View Source
var ErrHandshake = errors.New("plugin handshake")

ErrHandshake reports a malformed handshake line or handshake environment variable.

Functions

func DataDir added in v0.2.0

func DataDir(getenv func(string) string) string

DataDir returns the plugin's own data directory: EnvDataDir when the host sets it, otherwise HOME (older hosts give the plugin its own HOME with the same meaning). getenv is usually os.Getenv; an empty result means the plugin was not started by a host.

func FormatProtocols

func FormatProtocols(versions []int) string

FormatProtocols formats a list of major versions as the value of EnvProtocols.

func HostFeatures added in v0.2.0

func HostFeatures(getenv func(string) string) map[string]bool

HostFeatures returns the features listed in EnvHostFeatures. The result is empty, not nil, when the variable is missing (a host older than SDK v0.2.0): such a host supports none of them. getenv is usually os.Getenv.

func NegotiateProtocol

func NegotiateProtocol(host, plugin []int) (int, error)

NegotiateProtocol returns the highest major version both sides support, or ErrHandshake when there is none.

func ParseProtocols

func ParseProtocols(s string) ([]int, error)

ParseProtocols parses the value of EnvProtocols and returns the major versions sorted ascending without duplicates.

Types

type Handshake

type Handshake struct {
	Protocol int    // protocol major version chosen by the plugin
	Network  string // NetworkUnix or NetworkTCP
	Address  string // absolute socket path for unix; host:port for tcp
}

Handshake is the handshake line a plugin prints on stdout: <major version>|<network>|<address>.

func ParseHandshake

func ParseHandshake(line string) (Handshake, error)

ParseHandshake parses a handshake line (a trailing \n or \r\n is allowed). It checks only the format, not whether the host accepts the network and version.

func (Handshake) String

func (h Handshake) String() string

String returns the handshake line (without a newline).

Directories

Path Synopsis
examples
minimal command
Command minimal is the smallest useful plugin: a movie metadata provider with no configuration that knows one title.
Command minimal is the smallest useful plugin: a movie metadata provider with no configuration that knows one title.
people command
Command people is an example of the person and recognition capabilities: GetPerson with a gallery (IMAGE_TYPE_PHOTO) and attributes, SearchPerson confirmed by works, GetImages for people, and a Recognizer that reads catalog codes such as "ABC-123" from file names.
Command people is an example of the person and recognition capabilities: GetPerson with a gallery (IMAGE_TYPE_PHOTO) and attributes, SearchPerson confirmed by works, GetImages for people, and a Recognizer that reads catalog codes such as "ABC-123" from file names.
static command
Command static is the minimal example plugin: a metadata provider that returns fixed data (plus three demo services for images, subtitles and user sync, see services.go).
Command static is the minimal example plugin: a metadata provider that returns fixed data (plus three demo services for images, subtitles and user sync, see services.go).
Package hanconv converts text between Simplified and Traditional Chinese with the dictionaries of OpenCC (Open Chinese Convert, Apache License 2.0; see NOTICE).
Package hanconv converts text between Simplified and Traditional Chinese with the dictionaries of OpenCC (Open Chinese Convert, Apache License 2.0; see NOTICE).
Package i18n is a localization helper for plugin authors: it picks a string table by the BCP 47 language tag the host sends, and when that language is missing it looks strings up along a fixed fallback chain that always ends in English.
Package i18n is a localization helper for plugin authors: it picks a string table by the BCP 47 language tag the host sends, and when that language is missing it looks strings up along a fixed fallback chain that always ends in English.
Package plugintest is the conformance suite for Kryspal Media plugins.
Package plugintest is the conformance suite for Kryspal Media plugins.
Package pluginv1 holds the gRPC definitions of the Kryspal Media Plugin protocol v1 (generated from plugin.proto, metadata.proto, image.proto, subtitle.proto, usersync.proto, recognize.proto, segments.proto, danmaku.proto and host.proto) and the protocol constants.
Package pluginv1 holds the gRPC definitions of the Kryspal Media Plugin protocol v1 (generated from plugin.proto, metadata.proto, image.proto, subtitle.proto, usersync.proto, recognize.proto, segments.proto, danmaku.proto and host.proto) and the protocol constants.
sdk
Package sdk is the Kryspal Media Plugin SDK for Go plugin authors: it performs the handshake, creates the Unix socket, starts the gRPC server (including the standard health check), and shuts down gracefully on SIGTERM.
Package sdk is the Kryspal Media Plugin SDK for Go plugin authors: it performs the handshake, creates the Unix socket, starts the gRPC server (including the standard health check), and shuts down gracefully on SIGTERM.
httpclient
Package httpclient is the HTTP client most plugins need to talk to an upstream service politely: one request at a time, a minimum interval between requests, a recognizable User-Agent, a cap on the size of an answer, and the shared mapping of upstream failures to the errors the host understands (5xx and connection errors are UNAVAILABLE; a 429 becomes a rate limit with the Retry-After seconds).
Package httpclient is the HTTP client most plugins need to talk to an upstream service politely: one request at a time, a minimum interval between requests, a recognizable User-Agent, a cap on the size of an answer, and the shared mapping of upstream failures to the errors the host understands (5xx and connection errors are UNAVAILABLE; a 429 becomes a rate limit with the Retry-After seconds).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL