Skip to main content

script/dom/
eventsource.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::mem;
7use std::rc::Rc;
8use std::str::{Chars, FromStr};
9use std::time::Duration;
10
11use dom_struct::dom_struct;
12use encoding_rs::{Decoder, UTF_8};
13use headers::ContentType;
14use http::StatusCode;
15use http::header::{self, HeaderName, HeaderValue};
16use js::context::JSContext;
17use js::jsval::UndefinedValue;
18use js::rust::HandleObject;
19use mime::{self, Mime};
20use net_traits::request::{CacheMode, CorsSettings, Destination, RequestBuilder, RequestId};
21use net_traits::{FetchMetadata, FilteredMetadata, NetworkError, ResourceFetchTiming};
22use script_bindings::cell::DomRefCell;
23use script_bindings::conversions::SafeToJSValConvertible;
24use script_bindings::reflector::reflect_weak_referenceable_dom_object_with_proto;
25use servo_url::ServoUrl;
26use stylo_atoms::Atom;
27
28use crate::dom::bindings::codegen::Bindings::EventSourceBinding::{
29    EventSourceInit, EventSourceMethods,
30};
31use crate::dom::bindings::error::{Error, Fallible};
32use crate::dom::bindings::inheritance::Castable;
33use crate::dom::bindings::refcounted::Trusted;
34use crate::dom::bindings::reflector::DomGlobal;
35use crate::dom::bindings::root::DomRoot;
36use crate::dom::bindings::str::DOMString;
37use crate::dom::csp::{GlobalCspReporting, Violation};
38use crate::dom::event::Event;
39use crate::dom::eventtarget::EventTarget;
40use crate::dom::globalscope::GlobalScope;
41use crate::dom::messageevent::MessageEvent;
42use crate::dom::performance::performanceresourcetiming::InitiatorType;
43use crate::fetch::{FetchCanceller, RequestWithGlobalScope, create_a_potential_cors_request};
44use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
45use crate::realms::enter_auto_realm;
46use crate::timers::OneshotTimerCallback;
47
48const DEFAULT_RECONNECTION_TIME: Duration = Duration::from_millis(5000);
49
50#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
51struct GenerationId(u32);
52
53#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
54/// <https://html.spec.whatwg.org/multipage/#dom-eventsource-readystate>
55enum ReadyState {
56    Connecting = 0,
57    Open = 1,
58    Closed = 2,
59}
60
61#[derive(JSTraceable, MallocSizeOf)]
62struct DroppableEventSource {
63    canceller: DomRefCell<FetchCanceller>,
64}
65
66impl DroppableEventSource {
67    pub(crate) fn new(canceller: DomRefCell<FetchCanceller>) -> Self {
68        DroppableEventSource { canceller }
69    }
70
71    pub(crate) fn cancel(&self) {
72        self.canceller.borrow_mut().abort();
73    }
74
75    pub(crate) fn set_canceller(&self, data: FetchCanceller) {
76        *self.canceller.borrow_mut() = data;
77    }
78}
79
80// https://html.spec.whatwg.org/multipage/#garbage-collection-2
81impl Drop for DroppableEventSource {
82    fn drop(&mut self) {
83        // If an EventSource object is garbage collected while its connection is still open,
84        // the user agent must abort any instance of the fetch algorithm opened by this EventSource.
85        self.cancel();
86    }
87}
88
89#[dom_struct]
90pub(crate) struct EventSource {
91    eventtarget: EventTarget,
92    #[no_trace]
93    url: ServoUrl,
94    #[no_trace]
95    request: DomRefCell<Option<RequestBuilder>>,
96    last_event_id: DomRefCell<DOMString>,
97    reconnection_time: Cell<Duration>,
98    generation_id: Cell<GenerationId>,
99
100    ready_state: Cell<ReadyState>,
101    with_credentials: bool,
102    droppable: DroppableEventSource,
103}
104
105#[derive(Clone, MallocSizeOf)]
106enum ParserState {
107    Field,
108    Comment,
109    Value,
110    Eol,
111}
112
113#[derive(MallocSizeOf)]
114struct EventSourceContext {
115    decoder: Decoder,
116    event_source: Trusted<EventSource>,
117    gen_id: GenerationId,
118    parser_state: ParserState,
119    field: String,
120    value: String,
121    origin: String,
122    event_type: String,
123    data: String,
124    last_event_id: String,
125}
126
127impl Clone for EventSourceContext {
128    fn clone(&self) -> Self {
129        EventSourceContext {
130            decoder: UTF_8.new_decoder_with_bom_removal(),
131            event_source: self.event_source.clone(),
132            gen_id: self.gen_id,
133            parser_state: self.parser_state.clone(),
134            field: self.field.clone(),
135            value: self.value.clone(),
136            origin: self.origin.clone(),
137            event_type: self.event_type.clone(),
138            data: self.data.clone(),
139            last_event_id: self.last_event_id.clone(),
140        }
141    }
142}
143
144impl EventSourceContext {
145    /// <https://html.spec.whatwg.org/multipage/#announce-the-connection>
146    fn announce_the_connection(&self) {
147        let event_source = self.event_source.root();
148        if self.gen_id != event_source.generation_id.get() {
149            return;
150        }
151        let global = event_source.global();
152        let event_source = self.event_source.clone();
153        global.task_manager().remote_event_task_source().queue(
154            task!(announce_the_event_source_connection: move |cx| {
155                let event_source = event_source.root();
156                if event_source.ready_state.get() != ReadyState::Closed {
157                    event_source.ready_state.set(ReadyState::Open);
158                    event_source.upcast::<EventTarget>().fire_event(cx, atom!("open"));
159                }
160            }),
161        );
162    }
163
164    /// <https://html.spec.whatwg.org/multipage/#fail-the-connection>
165    fn fail_the_connection(&self) {
166        let event_source = self.event_source.root();
167        if self.gen_id != event_source.generation_id.get() {
168            return;
169        }
170        event_source.fail_the_connection();
171    }
172
173    /// <https://html.spec.whatwg.org/multipage/#reestablish-the-connection>
174    fn reestablish_the_connection(&self) {
175        let event_source = self.event_source.root();
176
177        if self.gen_id != event_source.generation_id.get() {
178            return;
179        }
180
181        let trusted_event_source = self.event_source.clone();
182        let global = event_source.global();
183        let event_source_context = EventSourceContext {
184            decoder: UTF_8.new_decoder_with_bom_removal(),
185            event_source: self.event_source.clone(),
186            gen_id: self.gen_id,
187            parser_state: ParserState::Eol,
188            field: String::new(),
189            value: String::new(),
190            origin: self.origin.clone(),
191            event_type: String::new(),
192            data: String::new(),
193            last_event_id: String::from(event_source.last_event_id.borrow().clone()),
194        };
195        global.task_manager().remote_event_task_source().queue(
196            task!(reestablish_the_event_source_onnection: move |cx| {
197                let event_source = trusted_event_source.root();
198
199                // Step 1.1.
200                if event_source.ready_state.get() == ReadyState::Closed {
201                    return;
202                }
203
204                // Step 1.2.
205                event_source.ready_state.set(ReadyState::Connecting);
206
207                // Step 1.3.
208                event_source.upcast::<EventTarget>().fire_event(cx, atom!("error"));
209
210                // Step 2.
211                let duration = event_source.reconnection_time.get();
212
213                // Step 3.
214                // TODO: Optionally wait some more.
215
216                // Steps 4-5.
217                let callback = OneshotTimerCallback::EventSourceTimeout(
218                    EventSourceTimeoutCallback {
219                        event_source: trusted_event_source,
220                        event_source_context,
221                    }
222                );
223                event_source.global().schedule_callback(callback, duration);
224            }),
225        );
226    }
227
228    /// <https://html.spec.whatwg.org/multipage/#processField>
229    fn process_field(&mut self) {
230        match &*self.field {
231            "event" => mem::swap(&mut self.event_type, &mut self.value),
232            "data" => {
233                self.data.push_str(&self.value);
234                self.data.push('\n');
235            },
236            "id" if !self.value.contains('\0') => {
237                mem::swap(&mut self.last_event_id, &mut self.value);
238            },
239            "retry" => {
240                if let Ok(time) = u64::from_str(&self.value) {
241                    self.event_source
242                        .root()
243                        .reconnection_time
244                        .set(Duration::from_millis(time));
245                }
246            },
247            _ => (),
248        }
249
250        self.field.clear();
251        self.value.clear();
252    }
253
254    /// <https://html.spec.whatwg.org/multipage/#dispatchMessage>
255    fn dispatch_event(&mut self, cx: &mut JSContext) {
256        let event_source = self.event_source.root();
257        // Step 1
258        *event_source.last_event_id.safe_borrow_mut(cx.no_gc()) =
259            DOMString::from(self.last_event_id.clone());
260        // Step 2
261        if self.data.is_empty() {
262            self.data.clear();
263            self.event_type.clear();
264            return;
265        }
266        // Step 3
267        if let Some(last) = self.data.pop() &&
268            last != '\n'
269        {
270            self.data.push(last);
271        }
272        // Step 6
273        let type_ = if !self.event_type.is_empty() {
274            Atom::from(self.event_type.clone())
275        } else {
276            atom!("message")
277        };
278        // Steps 4-5
279        let event = {
280            let mut realm = enter_auto_realm(cx, &*event_source);
281            let cx = &mut realm.current_realm();
282            rooted!(&in(cx) let mut data = UndefinedValue());
283            self.data.safe_to_jsval(cx, data.handle_mut());
284            MessageEvent::new(
285                cx,
286                &event_source.global(),
287                type_,
288                false,
289                false,
290                data.handle(),
291                DOMString::from(self.origin.clone()),
292                None,
293                event_source.last_event_id.borrow().clone(),
294                Vec::with_capacity(0),
295            )
296        };
297        // Step 7
298        self.event_type.clear();
299        self.data.clear();
300
301        // Step 8.
302        let global = event_source.global();
303        let event_source = self.event_source.clone();
304        let event = Trusted::new(&*event);
305        global.task_manager().remote_event_task_source().queue(
306            task!(dispatch_the_event_source_event: move |cx| {
307                let event_source = event_source.root();
308                if event_source.ready_state.get() != ReadyState::Closed {
309                    event.root().upcast::<Event>().fire(cx, event_source.upcast());
310                }
311            }),
312        );
313    }
314
315    /// <https://html.spec.whatwg.org/multipage/#event-stream-interpretation>
316    fn parse(&mut self, cx: &mut JSContext, stream: Chars) {
317        let mut stream = stream.peekable();
318
319        while let Some(ch) = stream.next() {
320            match (ch, &self.parser_state) {
321                (':', &ParserState::Eol) => self.parser_state = ParserState::Comment,
322                (':', &ParserState::Field) => {
323                    self.parser_state = ParserState::Value;
324                    if let Some(&' ') = stream.peek() {
325                        stream.next();
326                    }
327                },
328
329                ('\n', &ParserState::Value) => {
330                    self.parser_state = ParserState::Eol;
331                    self.process_field();
332                },
333                ('\r', &ParserState::Value) => {
334                    if let Some(&'\n') = stream.peek() {
335                        continue;
336                    }
337                    self.parser_state = ParserState::Eol;
338                    self.process_field();
339                },
340
341                ('\n', &ParserState::Field) => {
342                    self.parser_state = ParserState::Eol;
343                    self.process_field();
344                },
345                ('\r', &ParserState::Field) => {
346                    if let Some(&'\n') = stream.peek() {
347                        continue;
348                    }
349                    self.parser_state = ParserState::Eol;
350                    self.process_field();
351                },
352
353                ('\n', &ParserState::Eol) => self.dispatch_event(cx),
354                ('\r', &ParserState::Eol) => {
355                    if let Some(&'\n') = stream.peek() {
356                        continue;
357                    }
358                    self.dispatch_event(cx);
359                },
360
361                ('\n', &ParserState::Comment) => self.parser_state = ParserState::Eol,
362                ('\r', &ParserState::Comment) => {
363                    if let Some(&'\n') = stream.peek() {
364                        continue;
365                    }
366                    self.parser_state = ParserState::Eol;
367                },
368
369                (_, &ParserState::Field) => self.field.push(ch),
370                (_, &ParserState::Value) => self.value.push(ch),
371                (_, &ParserState::Eol) => {
372                    self.parser_state = ParserState::Field;
373                    self.field.push(ch);
374                },
375                (_, &ParserState::Comment) => (),
376            }
377        }
378    }
379}
380
381impl FetchResponseListener for EventSourceContext {
382    fn should_invoke(&self) -> bool {
383        self.event_source.root().generation_id.get() == self.gen_id
384    }
385
386    fn process_request_body(&mut self, _: RequestId) {
387        // TODO
388    }
389
390    fn process_response(
391        &mut self,
392        _: &mut JSContext,
393        _: RequestId,
394        metadata: Result<FetchMetadata, NetworkError>,
395    ) {
396        match metadata {
397            Ok(fm) => {
398                let meta = match fm {
399                    FetchMetadata::Unfiltered(m) => m,
400                    FetchMetadata::Filtered { unsafe_, filtered } => match filtered {
401                        FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_) => {
402                            return self.fail_the_connection();
403                        },
404                        _ => unsafe_,
405                    },
406                };
407                // Step 15.3 if res's status is not 200, or if res's `Content-Type` is not
408                // `text/event-stream`, then fail the connection.
409                if meta.status.code() != StatusCode::OK {
410                    return self.fail_the_connection();
411                }
412                let mime = match meta.content_type {
413                    None => return self.fail_the_connection(),
414                    Some(ct) => <ContentType as Into<Mime>>::into(ct.into_inner()),
415                };
416                if (mime.type_(), mime.subtype()) != (mime::TEXT, mime::EVENT_STREAM) {
417                    return self.fail_the_connection();
418                }
419                self.origin = meta.final_url.origin().ascii_serialization();
420                // Step 15.4 announce the connection and interpret res's body line by line.
421                self.announce_the_connection();
422            },
423            Err(error) => {
424                // Step 15.2 if res is a network error, then reestablish the connection, unless
425                // the user agent knows that to be futile, in which case the user agent may
426                // fail the connection.
427                if error.is_permanent_failure() {
428                    self.fail_the_connection()
429                } else {
430                    self.reestablish_the_connection()
431                }
432            },
433        }
434    }
435
436    fn process_response_chunk(&mut self, cx: &mut JSContext, _: RequestId, chunk: Vec<u8>) {
437        let mut output = String::with_capacity(chunk.len());
438        let mut input = &chunk[..];
439
440        loop {
441            if input.is_empty() {
442                return;
443            }
444            let (result, bytes_read) =
445                self.decoder
446                    .decode_to_string_without_replacement(input, &mut output, false);
447            match result {
448                encoding_rs::DecoderResult::InputEmpty => {
449                    self.parse(cx, output.chars());
450                    return;
451                },
452                encoding_rs::DecoderResult::Malformed(_, _) => {
453                    self.parse(cx, output.chars());
454                    self.parse(cx, "\u{FFFD}".chars());
455                    output.clear();
456                    input = &input[bytes_read..];
457                },
458                encoding_rs::DecoderResult::OutputFull => {
459                    self.parse(cx, output.chars());
460                    output.clear();
461                    input = &input[bytes_read..];
462                },
463            }
464        }
465    }
466
467    fn process_response_eof(
468        mut self,
469        cx: &mut JSContext,
470        _: RequestId,
471        response: Result<(), NetworkError>,
472        timing: ResourceFetchTiming,
473    ) {
474        let mut output = String::new();
475        let (result, _) = self
476            .decoder
477            .decode_to_string_without_replacement(&[], &mut output, true);
478        if !output.is_empty() {
479            self.parse(cx, "\u{FFFD}".chars());
480        }
481        if matches!(result, encoding_rs::DecoderResult::Malformed(_, _)) {
482            self.parse(cx, "\u{FFFD}".chars());
483        }
484        if response.is_ok() {
485            self.reestablish_the_connection();
486        }
487
488        network_listener::submit_timing(cx, &self, &response, &timing);
489    }
490
491    fn process_csp_violations(
492        &mut self,
493        cx: &mut JSContext,
494        _request_id: RequestId,
495        violations: Vec<Violation>,
496    ) {
497        let global = &self.resource_timing_global();
498        global.report_csp_violations(cx, violations, None, None);
499    }
500}
501
502impl ResourceTimingListener for EventSourceContext {
503    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
504        (InitiatorType::Other, self.event_source.root().url().clone())
505    }
506
507    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
508        self.event_source.root().global()
509    }
510}
511
512impl EventSource {
513    fn new_inherited(url: ServoUrl, with_credentials: bool) -> EventSource {
514        EventSource {
515            eventtarget: EventTarget::new_inherited(),
516            url,
517            request: DomRefCell::new(None),
518            last_event_id: DomRefCell::new(DOMString::from("")),
519            reconnection_time: Cell::new(DEFAULT_RECONNECTION_TIME),
520            generation_id: Cell::new(GenerationId(0)),
521
522            ready_state: Cell::new(ReadyState::Connecting),
523            with_credentials,
524            droppable: DroppableEventSource::new(DomRefCell::new(Default::default())),
525        }
526    }
527
528    fn new(
529        cx: &mut JSContext,
530        global: &GlobalScope,
531        proto: Option<HandleObject>,
532        url: ServoUrl,
533        with_credentials: bool,
534    ) -> DomRoot<EventSource> {
535        reflect_weak_referenceable_dom_object_with_proto(
536            cx,
537            Rc::new(EventSource::new_inherited(url, with_credentials)),
538            global,
539            proto,
540        )
541    }
542
543    // https://html.spec.whatwg.org/multipage/#sse-processing-model:fail-the-connection-3
544    pub(crate) fn cancel(&self) {
545        self.droppable.cancel();
546        self.fail_the_connection();
547    }
548
549    /// <https://html.spec.whatwg.org/multipage/#fail-the-connection>
550    pub(crate) fn fail_the_connection(&self) {
551        let global = self.global();
552        let event_source = Trusted::new(self);
553        global.task_manager().remote_event_task_source().queue(
554            task!(fail_the_event_source_connection: move |cx| {
555                let event_source = event_source.root();
556                if event_source.ready_state.get() != ReadyState::Closed {
557                    event_source.ready_state.set(ReadyState::Closed);
558                    event_source.upcast::<EventTarget>().fire_event(cx, atom!("error"));
559                }
560            }),
561        );
562    }
563
564    pub(crate) fn request(&self) -> RequestBuilder {
565        self.request.borrow().clone().unwrap()
566    }
567
568    pub(crate) fn url(&self) -> &ServoUrl {
569        &self.url
570    }
571}
572
573impl EventSourceMethods<crate::DomTypeHolder> for EventSource {
574    /// <https://html.spec.whatwg.org/multipage/#dom-eventsource>
575    fn Constructor(
576        cx: &mut JSContext,
577        global: &GlobalScope,
578        proto: Option<HandleObject>,
579        url: DOMString,
580        event_source_init: &EventSourceInit,
581    ) -> Fallible<DomRoot<EventSource>> {
582        // Step 2. Let settings be the relevant settings object for the `EventSource` constructor.
583        // Bindings pass that environment as `global`.
584        // Step 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings.
585        let url_record = match global.encoding_parse_a_url(&url.str()) {
586            Ok(u) => u,
587            // Step 4 If urlRecord is failure, then throw a "SyntaxError" DOMException.
588            Err(_) => return Err(Error::Syntax(None)),
589        };
590        // Step 1 Let ev be a new EventSource object.
591        let event_source = EventSource::new(
592            cx,
593            global,
594            proto,
595            // Step 5 Set ev's url to urlRecord.
596            url_record.clone(),
597            event_source_init.withCredentials,
598        );
599        global.track_event_source(&event_source);
600        let cors_attribute_state = if event_source_init.withCredentials {
601            // Step 7 If the value of eventSourceInitDict's withCredentials member is true,
602            // then set corsAttributeState to Use Credentials and set ev's withCredentials
603            // attribute to true.
604            CorsSettings::UseCredentials
605        } else {
606            // Step 6 Let corsAttributeState be Anonymous.
607            CorsSettings::Anonymous
608        };
609        // Step 8 Let request be the result of creating a potential-CORS request
610        // given urlRecord, the empty string, and corsAttributeState.
611        // Step 9. Set request's client to the environment settings object and other state;
612        // `with_global_scope` supplies client, origin, policy container, etc.
613        let mut request = create_a_potential_cors_request(
614            global.webview_id(),
615            url_record,
616            Destination::None,
617            Some(cors_attribute_state),
618            Some(true),
619            global.get_referrer(),
620        )
621        .with_global_scope(global);
622
623        // Step 10 User agents may set (`Accept`, `text/event-stream`) in request's header list.
624        // TODO(eijebong): Replace once typed headers allow it
625        request.headers.insert(
626            header::ACCEPT,
627            HeaderValue::from_static("text/event-stream"),
628        );
629        // Step 11 Set request's cache mode to "no-store".
630        request.cache_mode = CacheMode::NoStore;
631        // Step 13 Set ev's request to request.
632        *event_source.request.safe_borrow_mut(cx.no_gc()) = Some(request.clone());
633        // Step 14 Let processEventSourceEndOfBody given response res be the following step:
634        // if res is not a network error, then reestablish the connection.
635
636        event_source.droppable.set_canceller(FetchCanceller::new(
637            request.id,
638            false,
639            global.core_resource_thread(),
640        ));
641
642        let context = EventSourceContext {
643            decoder: UTF_8.new_decoder_with_bom_removal(),
644            event_source: Trusted::new(&event_source),
645            gen_id: event_source.generation_id.get(),
646            parser_state: ParserState::Eol,
647            field: String::new(),
648            value: String::new(),
649            origin: String::new(),
650
651            event_type: String::new(),
652            data: String::new(),
653            last_event_id: String::new(),
654        };
655
656        let task_source = global.task_manager().networking_task_source().into();
657        global.fetch(request, context, task_source);
658
659        // Step 16 Return ev.
660        Ok(event_source)
661    }
662
663    // https://html.spec.whatwg.org/multipage/#handler-eventsource-onopen
664    event_handler!(open, GetOnopen, SetOnopen);
665
666    // https://html.spec.whatwg.org/multipage/#handler-eventsource-onmessage
667    event_handler!(message, GetOnmessage, SetOnmessage);
668
669    // https://html.spec.whatwg.org/multipage/#handler-eventsource-onerror
670    event_handler!(error, GetOnerror, SetOnerror);
671
672    /// <https://html.spec.whatwg.org/multipage/#dom-eventsource-url>
673    fn Url(&self) -> DOMString {
674        DOMString::from(self.url.as_str())
675    }
676
677    /// <https://html.spec.whatwg.org/multipage/#dom-eventsource-withcredentials>
678    fn WithCredentials(&self) -> bool {
679        self.with_credentials
680    }
681
682    /// <https://html.spec.whatwg.org/multipage/#dom-eventsource-readystate>
683    fn ReadyState(&self) -> u16 {
684        self.ready_state.get() as u16
685    }
686
687    /// <https://html.spec.whatwg.org/multipage/#dom-eventsource-close>
688    fn Close(&self) {
689        let GenerationId(prev_id) = self.generation_id.get();
690        self.generation_id.set(GenerationId(prev_id + 1));
691        self.droppable.cancel();
692        self.ready_state.set(ReadyState::Closed);
693    }
694}
695
696#[derive(JSTraceable, MallocSizeOf)]
697pub(crate) struct EventSourceTimeoutCallback {
698    #[ignore_malloc_size_of = "Because it is non-owning"]
699    event_source: Trusted<EventSource>,
700    #[no_trace]
701    event_source_context: EventSourceContext,
702}
703
704impl EventSourceTimeoutCallback {
705    /// <https://html.spec.whatwg.org/multipage/#reestablish-the-connection>
706    pub(crate) fn invoke(self) {
707        let event_source = self.event_source.root();
708        let global = event_source.global();
709
710        // Step 5.1: If the EventSource object's readyState attribute is not set to CONNECTING, then return.
711        if event_source.ready_state.get() != ReadyState::Connecting {
712            return;
713        }
714
715        // Step 5.2: Let request be the EventSource object's request.
716        let mut request = event_source.request();
717
718        // Step 5.3: If the EventSource object's last event ID string is not the empty string, then:
719        //  - Let lastEventIDValue be the EventSource object's last event ID string, encoded as UTF-8.
720        //  - Set (`Last-Event-ID`, lastEventIDValue) in request's header list.
721        if !event_source.last_event_id.borrow().is_empty() {
722            // TODO(eijebong): Change this once typed header support custom values
723            request.headers.insert(
724                HeaderName::from_static("last-event-id"),
725                HeaderValue::from_str(&String::from(event_source.last_event_id.borrow().clone()))
726                    .unwrap(),
727            );
728        }
729
730        // Step 5.4: Fetch request and process the response obtained in this fashion, if
731        // any, as described earlier in this section.
732        let task_source = global.task_manager().networking_task_source().into();
733        global.fetch(request, self.event_source_context, task_source);
734    }
735}