YAHC(3pm)

πŸ“¦
version 0.035 Β· 2 versions
  • 0.035 Β· debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking (latest)
  • 0.035 Β· ubuntu jammy
, in debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking

🌸 YAHC β€” yet another HTTP client (that's the YAHC(3pm) page bestie, perl-versioned and everything) 🌸


✨ NAME ✨

YAHC β€” Yet another HTTP client πŸ’…


πŸŽ€ SYNOPSIS πŸŽ€

okay so here's the whole vibe in code, no notes:

use YAHC qw/yahc_reinit_conn/;

my @hosts = ('www.booking.com', 'www.google.com:80');
my ($yahc, $yahc_storage) = YAHC->new({ host => \@hosts });

$yahc->request({ path => '/', host => 'www.reddit.com' });
$yahc->request({ path => '/', host => sub { 'www.reddit.com' } });
$yahc->request({ path => '/', host => \@hosts });
$yahc->request({ path => '/', callback => sub { ... } });
$yahc->request({ path => '/' });
$yahc->request({
    path => '/',
    callback => sub {
        yahc_reinit_conn($_[0], { host => 'www.newtarget.com' })
            if $_[0]->{response}{status} == 301;
    }
});

$yahc->run;

πŸ’– DESCRIPTION πŸ’–

so YAHC is a fast & minimal low-level asynchronous HTTP client, and it's meant for the situation where you control both the client AND the server. Like, you own the whole group chat, basically. It especially suits cases where a set of requests needs to be executed against a group of machines β€” that's literally its moment. πŸ‘‘

it is NOT a general HTTP user agent. it does not support redirects, proxies, or any number of other advanced HTTP features like β€” in roughly descending order of feature completeness β€” LWP::UserAgent, WWW::Curl, HTTP::Tiny, HTTP::Lite or Furl. ngl this library is basically one step above manually talking HTTP over sockets. like, it's giving raw socket energy but with a lil glow-up. ✨

YAHC supports SSL and socket reuse (the latter is in experimental mode, so, yk, handle with care 🫢).


πŸ¦‹ STATE MACHINE πŸ¦‹

every YAHC connection goes through this exact list of states in its lifetime, and here's the diagram, don't touch it, it's iconic:

                  +-----------------+
              +<<-|   INITIALIZED   <-<<+
              v   +-----------------+   ^
              v           |             ^
              v   +-------v---------+   ^
              +<<-+   RESOLVE DNS   +->>+
              v   +-----------------+   ^
              v           |             ^
              v   +-------v---------+   ^
              +<<-+    CONNECTING   +->>+
              v   +-----------------+   ^
              v           |             ^
     Path in  v   +-------v---------+   ^  Retry
     case of  +<<-+    CONNECTED    +->>+  logic
     failure  v   +-----------------+   ^  path
              v           |             ^
              v   +-------v---------+   ^
              +<<-+     WRITING     +->>+
              v   +-----------------+   ^
              v           |             ^
              v   +-------v---------+   ^
              +<<-+     READING     +->>+
              v   +-----------------+   ^
              v           |             ^
              v   +-------v---------+   ^
              +>>->   USER ACTION   +->>+
                  +-----------------+
                          |
                  +-------v---------+
                  |    COMPLETED    |
                  +-----------------+

so there are three paths of workflow, bestie, let me break it down:

  1. Normal execution (the central line, the main character πŸ‘‘). in the normal situation, a connection after being initialized goes through these states:
    • RESOLVE DNS β€” not implemented πŸ™ˆ
    • CONNECTING β€” wait finishing of handshake
    • CONNECTED
    • WRITING β€” sending request body
    • READING β€” awaiting and reading response
    • USER ACTION β€” see below πŸ‘‡
    • COMPLETED β€” all done, this is terminal state, periodt πŸ’…

an SSL connection has an extra state SSL_HANDSHAKE after the CONNECTED state. also fyi the state 'RESOLVE DNS' is not implemented yet.

  1. Retry path (the right line, the comeback era ✨). in case of IO error during normal execution, YAHC retries the connection "retries" times. in practice this means that the connection goes back to the INITIALIZED state. it's giving round two, but make it resilient.
  2. Failure path (the left line 😭). if all retry attempts did not succeed, a connection goes to the state 'USER ACTION' (see below).

πŸ’œ State 'USER ACTION' πŸ’œ

the 'USER ACTION' state is called right before a connection is going to enter the 'COMPLETED' state (with either failed or successful results) and it's meant to give you, the user, a chance to interrupt the workflow. like a lil "wait, bestie, you sure?" moment. 🫢

the 'USER ACTION' state is entered in these circumstances:

  • HTTP response received. note that non-200 responses are NOT treated as error. πŸ‘€
  • unsupported HTTP response is received (such as a response without a Content-Length header)
  • retries limit reached
  • lifetime timeout has expired
  • provided callback has thrown exception
  • internal error has occurred

when a connection enters this state, the "callback" CodeRef is called:

$yahc->request({
    ...
    callback => sub {
        my (
            $conn,          # connection 'object'
            $error,         # one of YAHC::Error::* constants
            $strerror       # string representation of error
        ) = @_;

        # Note that fields in $conn->{response} are not reliable
        # if $error != YAHC::Error::NO_ERROR()

        # HTTP response is stored in $conn->{response}.
        # It can be also accessed via yahc_conn_response().
        my $response = $conn->{response};
        my $status = $response->{status};
        my $body = $response->{body};
    }
});

if there was no IO error, "yahc_conn_response" returns a "HashRef" representing the response. it contains these key-value pairs, verbatim:

proto         => :Str
status        => :StatusCode
body          => :Str
head          => :HashRef

in case of an error or a non-200 HTTP response, "yahc_retry_conn" or "yahc_reinit_conn" may be called to give the request more chances to complete successfully (for example by following redirects or providing new target hosts). also β€” and this part matters β€” note that in case of an error, data returned by "yahc_conn_response" cannot be trusted. for example, if an IO error happened during receiving the HTTP body, headers would state a 200 response code. πŸ’€

YAHC lowercases header names returned in "head". this is done to comply with the RFC, which identifies HTTP headers as case-insensitive. 🌈

in some cases a connection cannot be retried anymore and the callback is called for information purposes only. you can spot this case because $error has the YAHC::Error::TERMINAL_ERROR() bit set. one can use the "yahc_terminal_error" helper to detect such a case. πŸ”

⚠️ note that "callback" should NOT throw an exception. if it does, the connection will be immediately closed. no cap. 😭

πŸŽ€ METHODS πŸŽ€

✨ new ✨

okay so bestie, this method is the whole vibe starter β€” it makes you a YAHC object and a lil buddy storage object to go with it:

my ($yahc, $yahc_storage) = YAHC->new();

the reason this is split in two? it's a radical way of solving literally every possible memleak that comes from cyclic references in callbacks. since all the callback references get kept in $yahc_storage, you're totally free to use the YAHC object inside a request callback:

my $yahc->request({
    callback => sub {
        $yahc->stop; # this is fine!!!
    },
});

but here's the catch, and it's on you, not us: you have to pinky-promise that both $yahc and $yahc_storage stay in the same scope. that way they get destroyed together. πŸ‘―

new also accepts every parameter that request supports, and those get inherited by all the requests. slay.

on top of all that, new has three extra parameters of its own: socket_cache, account_for_signals, and loop. let's get into them πŸ’…

🧺 socket_cache

socket_cache controls the socket reuse logic. by default? socket cache is off. if you want YAHC to reuse sockets you gotta set socket_cache to a HashRef:

my ($yahc, $yahc_storage) = YAHC->new({ socket_cache => {} });

when you do that, YAHC keeps unused sockets keyed on join($;, $$, $host, $port, $scheme). the $; is there so you can use the $socket_cache->{$$, $host, $port, $scheme} idiom to get at the cache. iykyk ✨

managing that cache? your job. setting the right request headers for keep-alive? also your job, bestie. πŸ™ˆ and FYI, YAHC does not cache a socket when there's an error, when it's HTTP/1.0, or when the server straight-up tells you to close the connection (i.e. header 'Connection' = 'close').

πŸ” loop

by default, every YAHC object rolls with its own EV eventloop. that's usually the move you want, since it gives you more accurate timing metrics. πŸ“ˆ

but β€” if your process is already running an eventloop, having an inner loop means the outer one just sits there waiting until the inner one finishes. awkward.

to dodge that, you can tell YAHC which eventloop to use:

my ($yahc, $storage) = YAHC->new({
    loop => EV::default_loop(), # use the default EV eventloop
});

with that, YAHC shares the same eventloop as everyone else β€” which means some operations get riskier and you should avoid them. for example, in most scenarios you should not use account_for_signals alongside loop, because only whatever is entering the eventloop should be setting the signal handlers. ⚠️

🚨 account_for_signals

another parameter that needs special attention, fr fr: account_for_signals. here's why:

excerpt from EV documentation
<http://search.cpan.org/~mlehmann/EV-4.22/EV.pm#PERL_SIGNALS>

While Perl signal handling (%SIG) is not affected by EV, the
behaviour with EV is as the same as any other C library: Perl-
signals will only be handled when Perl runs, which means your signal
handler might be invoked only the next time an event callback is
invoked.

in practice, this means none of your set %SIG handlers get called until EV calls one of the perl callbacks β€” and sometimes that takes a long time. 😭 by setting account_for_signals, YAHC adds an EV::check watcher with an empty callback, which effectively makes EV call the callback on every single iteration. cute trick, but it does come at a performance cost. here's what the EV docs say about it:

... you can also force a watcher to be called on every event loop
iteration by installing a EV::check watcher. This ensures that perl
gets into control for a short time to handle any pending signals,
and also ensures (slightly) slower overall operation.

so: if your code β€” or the code surrounding your code β€” uses %SIG handlers, it's wise to set account_for_signals. πŸ’…


🌸 request 🌸

protocol               => "HTTP/1.1", # (or "HTTP/1.0")
scheme                 => "http" or "https"
host                   => see below,
port                   => ...,
method                 => "GET",
path                   => "/",
query_string           => "",
head                   => [],
body                   => "",

# timeouts
connect_timeout        => undef,
request_timeout        => undef,
drain_timeout          => undef,
lifetime_timeout       => undef,

# burst control
backoff_delay          => undef,

# callbacks
init_callback          => undef,
connecting_callback    => undef,
connected_callback     => undef,
writing_callback       => undef,
reading_callback       => undef,
callback               => undef,

# SSL options
ssl_options            => {},

notice how YAHC does not take a full URI string as input? nope. you have to hand over the individual parts of the URL yourself. if you've got an existing URI string that needs parsing into a request, go use the URI module. it's literally built for that. πŸ’–

for example, to send a request to http://example.com/flower?color=red, you pass these parameters:

$yach->request({
    host         => "example.com",
    port         => "80",
    path         => "/flower",
    query_string => "color=red"
});

πŸ› οΈ request building

YAHC doesn't escape anything for you, bestie β€” it just passes values through as-is. so yeah, you can very easily produce invalid requests if, say, any of those strings contain a newline, or aren't properly escaped otherwise. 🫠

also note: you do not need to put the leading "?" character in the query_string. you do, however, need to properly uri_escape the content of query_string.

the value of head is an ArrayRef of key-value pairs instead of a HashRef β€” that way you get to decide the order the headers go out in, and you can send the same header name multiple times. iconic. for example:

head => [
    "Content-Type" => "application/json",
    "X-Requested-With" => "YAHC",
]

will produce these request headers:

Content-Type: application/json
X-Requested-With: YAHC

🏠 host

the host parameter accepts one of these values:

  1. string β€” represents the target host. the string may have these formats: hostname:port, ip:port.
  2. ArrayRef of strings β€” YAHC will cycle through the items, selecting a new host for each attempt.
  3. CodeRef. the subroutine is invoked for each attempt and should at least return a string (hostname or IP address). it can also return an array containing: ($host, $ip, $port, $scheme). this option effectively gives the user control over host selection for retries. the CodeRef is passed the connection object, which can be fed to the yahc_conn_* family of functions.

⏱️ timeouts

the value of connect_timeout, request_timeout and drain_timeout is in floating point seconds, and it's used as the time limit for connecting to the host (reaching CONNECTED state), full request time (reaching COMPLETED state) and sending the request to the remote site (reaching READING state) respectively.

lifetime_timeout has a special purpose. its job is to provide an upper bound timeout for a request's lifetime. in other words, if a request comes with multiple retries, connect_timeout, request_timeout and drain_timeout are per attempt β€” but lifetime_timeout covers all the attempts. if by the time lifetime_timeout expires a connection isn't in COMPLETED state, an error is generated. ⚠️ and note: after this error the connection cannot be retried anymore. so it's forced to go to COMPLETED state.

the default value for all of them is undef, meaning no timeout limit.

🌊 backoff_delay

backoff_delay lets you introduce a delay between retries. it's a great way to avoid load spikes on the server side. this next example creates a new request that gets retried twice, doing three attempts in total, with the second and third attempts each delayed by one second:

$yach->request({
    host          => "example.com",
    retries       => 2,
    backoff_delay => 1,
});

backoff_delay can be set in two ways:

  1. floating point seconds β€” defines a constant delay between retries.
  2. CodeRef. the subroutine is invoked on each retry and should return floating point seconds. this option is useful for having an exponentially growing delay or, for instance, jitted delays.

the default value is undef, meaning no delay.

πŸ“ž callbacks

the value of init_callback, connecting_callback, connected_callback, writing_callback, reading_callback is a reference to a subroutine which gets called upon reaching the corresponding state. any exception thrown in the subroutine will be ignored. πŸ™ˆ

the value of callback defines the main request callback, which is called when a connection enters the 'USER ACTION' state (see the 'USER ACTION' state above).

also see LIMITATIONS. πŸ‘€

πŸ” ssl_options

doing HTTPS requires the value of ssl_options to be extended by two parameters set to the current hostname:

SSL_verifycn_name => $hostname,
IO::Socket::SSL->can_client_sni ? ( SSL_hostname => $hostname ) : (),

apart from those changes, the value is passed directly to IO::Socket::SSL::start_SSL(). for more details refer to the IO::Socket::SSL documentation <https://metacpan.org/pod/IO::Socket::SSL>. 🌈


πŸ’§ drop πŸ’§

given a connection HashRef or a conn_id, this moves the connection to COMPLETED state (skipping right past the 'USER ACTION' state) and drops it from the internal pool. it takes two parameters: the first is either a connection id or a connection HashRef. the second is a boolean flag saying whether the connection's socket should be closed or whether it might be reused. πŸ’…

πŸƒ run πŸƒ

start YAHC's loop. the loop stops when all connections complete.

note that run can accept two extra parameters: until_state and a list of connections. these two parameters tell YAHC to break the loop once the specified connections reach the desired state. for example:

$yahc->run(YAHC::State::READING(), $conn_id);

will loop until connection '$conn_id' moves to state READING, meaning the data has been sent to the remote side. in order to gather the response you should later call:

$yahc->run(YAHC::State::COMPLETED(), $conn_id);

or simply:

$yahc->run();

leaving the list of connections empty makes YAHC wait for all connections to reach the needed until_state.

and note: waiting on one particular connection to finish does not mean the others aren't executing. nope! instead, all active connections are looped at the same time β€” YAHC just breaks the loop once the waited-on connection reaches the needed state. πŸ”₯

⚑ run_once ⚑

same as run but with EV::RUN_ONCE set. for more details check <https://metacpan.org/pod/EV>.

🎟️ run_tick 🎟️

same as run but with EV::RUN_NOWAIT set. for more details check <https://metacpan.org/pod/EV>.

❓ is_running ❓

returns true if YAHC is running, false otherwise.

πŸ”„ loop πŸ”„

returns the underlying EV loop object.

πŸ›‘ break πŸ›‘

breaks the running EV loop, if there is any.

🌟 EXPORTED FUNCTIONS 🌟

πŸ’« yahc_reinit_conn

Ok bestie, this one reinitializes the connection you hand it. The attempt counter? Reset to 0, fresh start, we love a glow-up. πŸ’…

It takes a HashRef as a second argument, and by passing that in you can change host, port, scheme, body, head and other parameters. The format and meaning of all those params is exactly the same as in the request method, no cap.

One of the use cases, for example, is handling redirects:

use YAHC qw/yahc_reinit_conn/;

my ($yahc, $yahc_storage) = YAHC->new();
$yahc->request({
    host => 'domain_which_returns_301.com',
    callback => sub {
        ...
        my $conn = $_[0];
        yahc_reinit_conn($conn, { host => 'www.newtarget.com' })
            if $_[0]->{response}{status} == 301;
        ...
    }
});

$yahc->run;

yahc_reinit_conn is meant to be called inside callback, aka when the connection is chilling in the 'USER ACTION' state. That's its moment, its main character era. ✨

πŸ” yahc_retry_conn

Retries the connection. But listen β€” yahc_retry_conn should only be called if yahc_conn_attempts_left returns a positive value. Otherwise it just exits silently, like, no drama, no error, just vibes. πŸ™ˆ

It accepts a HashRef as a second argument, and by passing it you can change the backoff_delay parameter. See the docs for request for more deets on backoff_delay.

Intended usage is retrying transient failures, or trying a whole different host:

use YAHC qw/
    yahc_retry_conn
    yahc_conn_attempts_left
/;

my ($yahc, $yahc_storage) = YAHC->new();
$yahc->request({
    retries => 2,
    host => [ 'host1', 'host2' ],
    callback => sub {
        ...
        my $conn = $_[0];
        if ($_[0]->{response}{status} == 503 && yahc_conn_attempts_left($conn)) {
            yahc_retry_conn($conn);
            return;
        }
        ...
    }
});

$yahc->run;

yahc_retry_conn is meant to be called inside callback, same as yahc_reinit_conn. Twinsies. 🫢

πŸ†” yahc_conn_id

Returns the id of the connection you gave it.

🚦 yahc_conn_state

Returns the state of the connection you gave it.

🎯 yahc_conn_target

Returns the selected host and port for the current attempt of the given connection. Format is "host:port", and default port values are omitted (so no awkward :80 hanging around).

πŸ”— yahc_conn_url

Same as yahc_conn_target but it returns the full URL. Yeah. The whole thing. ✨

πŸ“¦ yahc_conn_user_data

Lets you associate arbitrary data with a connection. Very demure, very mindful. But β€” and I cannot stress this enough β€” be aware of not creating a cyclic reference! That's a memory leak bestie, we do not do that. πŸ’€

🚨 yahc_conn_errors

Returns the errors that appeared in the given connection. Note that it returns all errors, not only the ones that happened during the current attempt. It's the full receipts, the whole story.

The returned value is an ArrayRef of ArrayRefs. The later one (each inner one) represents an error and contains these items:

  1. error number (see YAHC::Error constants)
  2. error string
  3. ArrayRef of host, ip, port, scheme
  4. time when the error happened
  5. attempt when the error happened

πŸ“ yahc_conn_register_error

yahc_conn_register_error adds a new record to the connection's error list. This function is used internally for keeping track of all the low-level errors during a connection's lifetime β€” but you can absolutely use it too, for high-level errors like 50x responses. πŸ‘‘

It takes $conn, $error (which is one of the YAHC::Error constants) and an error description. The error description can be passed in sprintf manner. For example:

$yahc->request({
    ...
    callback => sub {
        ...
        my $conn = $_[0];
        my $status = $conn->{response}{status} || 0;
        if ($status == 503 || $status == 504) {
            yahc_conn_register_error(
                $conn,
                YAHC::Error::RESPONSE_ERROR(),
                "server returned %d",
                $status
            );

            yahc_retry_conn($conn);
            return;
        }
        ...
    }
});

πŸ” yahc_conn_last_error

Returns the last error that appeared in the connection. See yahc_conn_errors for the shape of it.

☠️ yahc_terminal_error

Given an error, it returns 1 if the error has the YAHC::Error::TERMINAL_ERROR() bit set. Otherwise it returns 0. Simple as that. πŸ’…

πŸ•°οΈ yahc_conn_timeline

Returns the timeline of the given connection. Wanna know more about the timeline? It's in the description of the new method, go peek. πŸ‘€

πŸ“¨ yahc_conn_request

Returns the request of the given connection. See request.

πŸ“¬ yahc_conn_response

Returns the response of the given connection. See request.

πŸ”’ yahc_conn_attempt

Returns the current attempt, starting from 1. The function can also return 0 if no attempts were made yet. (Zero attempts, zero thoughts, we've all been there.) πŸ¦‹

πŸ’― yahc_conn_attempts_left

Returns the number of attempts left.

🧦 yahc_conn_socket_cache_id

Returns the socket_cache id for the given connection. Should be used to generate a key for socket_cache. If the connection is not initialized yet, undef is returned. Fair warning. ✨


πŸ’₯ ERRORS πŸ’₯

YAHC provides a set of constants for errors. Each constant returns a bitmask, which you can use to detect the presence of a particular error β€” for example, inside callback. There is one exception: YAHC::Error::NO_ERROR() returns 0, indicating no error during request execution.

Your error handling code can look like this:

$yahc->request({
    ...
    callback => sub {
        my (
            $conn,          # connection 'object'
            $error,         # one of YAHC::Error::* constants
            $strerror       # string representation of error
        ) = @_;

        if ($error & YAHC::Error::TIMEOUT()) {
            # A timeout has happened. Use one of YAHC::Error::*_TIMEOUT()
            # constants for more clarification
        } elsif ($error & YAHC::Error::SSL_ERROR()) {
            # We had some issues with SSL. $error might have
            # YAHC::Error::READ_ERROR() or YAHC::Error::WRITE_ERROR()
            # indicating whether is was read or write error.
        } elsif (...) { # etc
        }
    }
});

Here's the list of error constants. The names are self-explanatory in many cases, periodt:

  • YAHC::Error::NO_ERROR() β€” returns value 0 (not a bitmask), meaning no error
  • YAHC::Error::REQUEST_TIMEOUT(), YAHC::Error::CONNECT_TIMEOUT(), YAHC::Error::DRAIN_TIMEOUT(), YAHC::Error::LIFETIME_TIMEOUT(), YAHC::Error::TIMEOUT(), YAHC::Error::RETRY_LIMIT() β€” the connection has exhausted all available retries. This error is usually returned to callback. Check the connection's errors via yahc_conn_errors to inspect the reasons of failures for each individual attempt. πŸ”Ž
  • YAHC::Error::CONNECT_ERROR(), YAHC::Error::READ_ERROR(), YAHC::Error::WRITE_ERROR(), YAHC::Error::SSL_ERROR(), YAHC::Error::REQUEST_ERROR() β€” not used
  • YAHC::Error::RESPONSE_ERROR() β€” server returned unparsable response 😭
  • YAHC::Error::CALLBACK_ERROR() β€” usually represents an exception in one of the callbacks
  • YAHC::Error::TERMINAL_ERROR() β€” this bit is set when the connection cannot be retried anymore and is forced to complete
  • YAHC::Error::INTERNAL_ERROR()

πŸ“š REPOSITORY πŸ“š

<https://github.com/ikruglov/YAHC>


πŸ“Œ NOTES πŸ“Œ

🌈 UTF8 flag

Not me warning you about this: YAHC has an astonishing reduction in performance if any parameters participating in building the HTTP message has the UTF8 flag set. Those fields are protocol, host, port, method, path, query_string, head, body and maybe others. It's giving flop era for your benchmarks. 😭

Just one example (check scripts/utf8_test.pl for the code). Simple HTTP request with 10MB of payload:

elapsed without utf8 flag: 0.039s
elapsed with utf8 flag: 0.540s

Because of this, YAHC warns if it detected UTF8-flagged payload. So the user needs to make sure that all data passed to YAHC is unflagged binary strings. All of it. Every last byte. No exceptions bestie. 🫢

🚧 LIMITATIONS

  • State 'RESOLVE DNS' is not implemented yet.

πŸ‘‘ AUTHORS πŸ‘‘

Ivan Kruglov <ivan.kruglov@yahoo.com>


Copyright (c) 2013-2017 Ivan Kruglov "<ivan.kruglov@yahoo.com>".


πŸ™ ACKNOWLEDGMENT πŸ™

This module derived lots of ideas, code and docs from Hijk <https://github.com/gugod/Hijk>. This module was originally developed for Booking.com. Slay. ✨


πŸ“œ LICENCE πŸ“œ

The MIT License


⚠️ DISCLAIMER OF WARRANTY ⚠️

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.


and that's the whole page bestie!! πŸŽ€ this little legend was written for perl v5.36.0 and dated 2022-12-13, and it lives at YAHC(3pm) β€” go forth and make requests responsibly, ily πŸ’–βœ¨

πŸ“¦ where this page comes from