Wpływ request abort na serwer

Wpływ request abort na serwer3 min read

Obecnie jest moda na aplikację reaktywne, responsywne etc. Tego typu aplikacje bardzo często odświeżają dane przy pomocy zapytań AJAX-owych. Weźmy na warsztat przypadek, w którym podczas request-u stan aplikacji frontend-owej ulegnie zmianie i request przestanie być aktualny. W takiej sytuacji programistom na pomoc przychodzi funkcją “abort” z prototypu obiektu XMLHttpRequest i to właśnie chciałbym omówić.

Opis działania funkcji abort

Zgodnie z dokumentacją zamieszczoną na Mozilla Developers Network (MDN) wywołanie funkcji abort na instancji XMLHttpRequest anuluje zapytania, które już zostały wysłane do serwera.

Skąd to się bierze? Jak przebiega cały proces? Magia jakaś czy co?

Wywołanie funkcji “abort” ustawia dwa bardzo ważne klucze w instancji XMLHttpRequest:

  • ReadyState na wartość 4 (XMLHttpRequest.DONE) co w skrócie oznacza zakończenie żądania
  • Status na wartość 0, co oznacza że request został ubity przez przeglądarkę (bardzo często przy błędach związanych z CORS-em dostajemy status 0)

Teoria jest piękna… w teorii. A jak wszyscy wiemy najlepiej uczyć się poprzez praktykę, więc…

…Piszemy aplikację testową!

Zaczynając od serwera

No dobrze, ale skąd ma serwer wiedzieć że nie musi obsługiwać tego zapytania?

To jest bardzo dobre pytanie! Jego autor dostał ode mnie pączka za wnikliwość. No dobra, sam je sobie zadałem… Pomijając jakość wyrobu cukierniczego (który swoją drogą ładniej wyglądał niż smakował) poczułem ogromną potrzebę zobrazowania działania kryjącego się za działaniem funkcji abort. Aby lepiej wszystko zilustrować napisałem prosty serwer w node.js, którego jedynym zadaniem jest zliczanie ile request-ów zostało wysłanych na podany endpoint. Do stworzenia tego posłużyłem się frameworkiem express.js, dzięki czemu całą implementację zmieściłem w 18 liniach kodu:

const express = require('express')
const app = express()

let api = 0;

app.use(express.static('static'))

app.get('/api', (req, res) => {
    let value = api++;

    setTimeout(() => {
        res.send(value + '')        
    }, 10)
})

app.listen(3000, () => {
    console.log('Example app listening on port 3000!')
})

W obsłudze odpowiedzi pod adresem “/api” wykorzystałem funkcję setTimeout, aby chociaż minimalnie zasymulować opóźnienie związane z przesyłaniem pakietów po sieci (zapytania AJAX-owe na localhoście są bardzo szybkie :))

Teraz czas na szybki opis stworzonego przeze mnie frontendu aplikacji

Tworzymy frontend

W katalogu static swojego projektu stworzyłem plik “index.html”, który posłuży mi do wysyłania i abortowania requestów. Cała implementacja tego zjawiska zajęła mi 21 lini kodu

<script>
{
    function sendRequest(doAbort = true) {
        var req = new XMLHttpRequest();
        req.open('GET', '/api', true); 
        req.send(null);

        if (doAbort) {
            req.abort();
        }
    }

    sendRequest(false);
    sendRequest();
    sendRequest();
    sendRequest();
    sendRequest();
    sendRequest();
    sendRequest(false);
}
</script>

Mam świadomość powtarzalności kodu przy wysyłaniu zapytań, które potem abortuje, jednak to jest tylko aplikacja typu “proof of concept”.

Więc jak wyglądają wyniki naszego małego eksperymentu?

Podsumowanie

Po wywołaniu aplikacji pod adresem http://localhost:3000/index.html w konsoli developerskiej Chrome-a ujrzałem wysłane requesty

Jak widać na załączonym obrazku poprawnie wysłano dwa requesty: pierwszy i ostatni. request zostały wysłane poprawne, natomiast wszystko pomiędzy zostało anulowane.

Pierwszy request oczywiście otrzymał zwrotkę z cyfrą zero (0)

Natomiast najciekawszy jest ostatni request, który pomoże mi zobrazować co się dzieje na serwerze. To co zostało zwrócone przez serwer możecie zobaczyć na poniższym obrazku

Serwer zwrócił cyfrę 6, co ujawnia smutną prawdę – poprzednie żądania zostały całkowicie przetworzone przez serwer 🙁

Słowo końcowe

Jak widać funkcja abort wcale nie ubija całego request-u, tylko wyłącza jego obsługę po stronie frontendu. Z tego powodu polecam wam rozejrzenie się za jakimś innym rozwiązaniem np. debounc-em.

Całą aplikację możecie znaleźć na moim Github-ie >>Tutaj<<

Mam nadzieje, że post się Wam przyda i do następnego!

Cześć!

By | 2017-09-08T16:59:08+00:00 Sierpień 25th, 2017|Frontend, Node|6 komentarzy
  • Grzegorz Kowalski

    Bardzo dobry post. Dzięki.

    • Dzięki! 🙂

  • Dzięki za artykuł. Świetnie, że poruszyłeś tą kwestię. Bardzo mi się przyda do rozwiązania problemu Race Condition w Angularze – stosowny projekt, który zrobiłem na potrzeby symulacji tego problemu: https://github.com/piecioshka/angular-rxjs-race-condition-problem

    Mam jedno pytanie, czy wiesz, co mógłby zrobić serwer, aby nie obsługiwać takiego klienta, który abortował request?

    PS. Zawsze muszę zwiększyć rozmiar tekstu do 150% gdy czytam Twojego bloga.
    Może warto pomyśleć o ludziach słabowidzących 😉

    • Hej 🙂 Dzięki,
      Co do braku obsługi żądania http serwer może wykonać sprawdzenie czy przeglądarka utrzymuje połączenie z serwerem np. dla node.js możemy to zrobić nasłuchując na event close na obiekcie request

      req.on('close', () => {
      console.log('request aborted');
      });

      A w .NET-cie możemy to sprawdzić za pomocą propertisa HttpResponse.IsClientConnected.
      W kodzie coś takiego wygląda dość brzydko, dlatego trzeba z rozwagą dekorować endpoint-y taką logiką 🙂

      PS. Zwiększyłem rozmiar fonta na blogu 🙂 Niedługo zaimplementuje wybór wielkości font-a na blogu 🙂

      • Arkadiusz Oleksy

        Trzeba zaznaczyć, że musiałbyś mieć dodatkowo jakiś system do rollbacku zmian w momencie gdy klient zerwie połączenie żeby to miało sens

        • Tak, dokładnie 🙂