หมดเวลาของคำขอ Fetch API หรือไม่


100

ฉันมีfetch-api POSTคำขอ:

fetch(url, {
  method: 'POST',
  body: formData,
  credentials: 'include'
})

ฉันต้องการทราบว่าการหมดเวลาเริ่มต้นสำหรับสิ่งนี้คืออะไร แล้วเราจะตั้งค่าให้เป็นค่าเฉพาะเช่น 3 วินาทีหรือไม่ จำกัด วินาทีได้อย่างไร

คำตอบ:


78

แก้ไข 1

ตามที่ระบุไว้ในความคิดเห็นรหัสในคำตอบเดิมจะยังคงทำงานตัวจับเวลาแม้ว่าสัญญาจะได้รับการแก้ไข / ปฏิเสธแล้วก็ตาม

โค้ดด้านล่างช่วยแก้ปัญหานั้น

function timeout(ms, promise) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error('TIMEOUT'))
    }, ms)

    promise
      .then(value => {
        clearTimeout(timer)
        resolve(value)
      })
      .catch(reason => {
        clearTimeout(timer)
        reject(reason)
      })
  })
}


คำตอบเดิม

ไม่มีค่าเริ่มต้นที่ระบุ ข้อกำหนดไม่ได้กล่าวถึงการหมดเวลาเลย

คุณสามารถใช้ Wrapper การหมดเวลาของคุณเองสำหรับสัญญาโดยทั่วไป:

// Rough implementation. Untested.
function timeout(ms, promise) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      reject(new Error("timeout"))
    }, ms)
    promise.then(resolve, reject)
  })
}

timeout(1000, fetch('/hello')).then(function(response) {
  // process response
}).catch(function(error) {
  // might be a timeout error
})

ตามที่อธิบายไว้ในhttps://github.com/github/fetch/issues/175 ความคิดเห็นโดยhttps://github.com/mislav


27
เหตุใดจึงเป็นคำตอบที่ได้รับการยอมรับ setTimeout ที่นี่จะดำเนินต่อไปแม้ว่าสัญญาจะแก้ไข ทางออกที่ดีกว่าคือทำสิ่งนี้: github.com/github/fetch/issues/175#issuecomment-216791333
radtad

3
@radtad Mislav ปกป้องวิธีการของเขาลดลงไปในหัวข้อที่: github.com/github/fetch/issues/175#issuecomment-284787564 ไม่สำคัญว่าการหมดเวลาจะดำเนินต่อไปเพราะการเรียกร้อง.reject()สัญญาที่ได้รับการแก้ไขแล้วไม่ได้ทำอะไรเลย
Mark Amery

1
แม้ว่าฟังก์ชัน "ดึงข้อมูล" จะถูกปฏิเสธโดยการหมดเวลา แต่การเชื่อมต่อ tcp ในพื้นหลังจะไม่ปิด ฉันจะออกจากกระบวนการโหนดอย่างสง่างามได้อย่างไร
Prog Quester

26
หยุด! นี่เป็นคำตอบที่ไม่ถูกต้อง! แม้ว่าจะดูเหมือนเป็นโซลูชันที่ดีและใช้งานได้จริง แต่จริงๆแล้วการเชื่อมต่อจะไม่ถูกปิดซึ่งในที่สุดก็ใช้การเชื่อมต่อ TCP (อาจจะไม่มีที่สิ้นสุด - ขึ้นอยู่กับเซิร์ฟเวอร์) ลองนึกภาพวิธีแก้ปัญหาที่ไม่ถูกต้องนี้ที่จะนำไปใช้ในระบบที่ลองการเชื่อมต่อใหม่ทุกช่วงเวลาซึ่งอาจนำไปสู่การขาดอินเตอร์เฟสของเครือข่าย (โอเวอร์โหลด) และทำให้เครื่องของคุณค้างในที่สุด! @Endless โพสต์คำตอบที่ถูกต้องที่นี่
Slavik Meltser

1
@SlavikMeltser ฉันไม่เข้าใจ คำตอบที่คุณชี้ไม่ทำลายการเชื่อมต่อ TCP เช่นกัน
Mateus Pires

143

ฉันชอบวิธีการที่สะอาดจากส่วนสำคัญนี้โดยใช้Promise.race

fetchWithTimeout.js

export default function (url, options, timeout = 7000) {
    return Promise.race([
        fetch(url, options),
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('timeout')), timeout)
        )
    ]);
}

main.js

import fetch from './fetchWithTimeout'

// call as usual or with timeout as 3rd argument

fetch('http://google.com', options, 5000) // throw after max 5 seconds timeout error
.then((result) => {
    // handle result
})
.catch((e) => {
    // handle errors and timeout error
})

2
ซึ่งทำให้เกิด "การปฏิเสธที่ไม่สามารถจัดการได้" หากfetchเกิดข้อผิดพลาดหลังจากหมดเวลา สิ่งนี้สามารถแก้ไขได้โดยจัดการ ( .catch) fetchความล้มเหลวและการเปลี่ยนใหม่หากยังไม่เกิดการหมดเวลา
lionello

5
IMHO นี้อาจจะละเอียดเพิ่มเติมที่ดีขึ้นกับ AbortController เมื่อปัดดูstackoverflow.com/a/47250621
RiZKiT

จะเป็นการดีกว่าถ้าล้างการหมดเวลาหากการดึงข้อมูลสำเร็จเช่นกัน
Bob9630

105

เมื่อใช้AbortControllerคุณจะสามารถทำได้:

const controller = new AbortController();
const signal = controller.signal;

const fetchPromise = fetch(url, {signal});

// 5 second timeout:
const timeoutId = setTimeout(() => controller.abort(), 5000);


fetchPromise.then(response => {
  // completed request before timeout fired

  // If you only wanted to timeout the request, not the response, add:
  // clearTimeout(timeoutId);
})

14
สิ่งนี้ดูดีกว่าการแก้ปัญหาตามสัญญาเนื่องจากอาจยกเลิกคำขอแทนที่จะตอบสนองก่อนหน้านี้ ช่วยแก้ให้ด้วยนะถ้าฉันผิด.
Karl Adler

3
คำตอบไม่ได้อธิบายว่า AbortController คืออะไร นอกจากนี้ยังอยู่ในระหว่างการทดลองและจำเป็นต้องเติม polyfilled ในเอ็นจิ้นที่ไม่รองรับและไม่ใช่ไวยากรณ์
Estus Flask

อาจไม่ได้อธิบายว่า AbortController คืออะไร (ฉันเพิ่มลิงค์ไปยังคำตอบเพื่อให้ง่ายขึ้นสำหรับคนขี้เกียจ) แต่นี่เป็นคำตอบที่ดีที่สุดเนื่องจากมันเน้นถึงความจริงที่ว่าการเพิกเฉยต่อคำขอไม่ได้หมายความว่ายังคงอยู่ ไม่รอดำเนินการ คำตอบที่ดี
Aurelio

2
"ฉันเพิ่มลิงค์ไปยังคำตอบเพื่อให้ง่ายขึ้นสำหรับคนขี้เกียจ" - ควรมาพร้อมกับลิงค์และข้อมูลเพิ่มเติมตามกฎ tbh แต่ขอขอบคุณสำหรับการปรับปรุงคำตอบ
Jay Wick

6
ดีกว่าที่จะมีคำตอบนี้ดีกว่าไม่มีคำตอบเพราะผู้คนถูกไล่โดย nitpickery, tbh
Michael Terry

21

จากคำตอบที่ยอดเยี่ยมของ Endless ฉันได้สร้างฟังก์ชันยูทิลิตี้ที่เป็นประโยชน์

const fetchTimeout = (url, ms, { signal, ...options } = {}) => {
    const controller = new AbortController();
    const promise = fetch(url, { signal: controller.signal, ...options });
    if (signal) signal.addEventListener("abort", () => controller.abort());
    const timeout = setTimeout(() => controller.abort(), ms);
    return promise.finally(() => clearTimeout(timeout));
};
  1. หากหมดเวลาก่อนที่จะดึงทรัพยากรระบบจะยกเลิกการดึงข้อมูล
  2. หากมีการดึงทรัพยากรก่อนถึงระยะหมดเวลาระบบจะล้างการหมดเวลา
  3. หากสัญญาณอินพุตถูกยกเลิกการดึงข้อมูลจะถูกยกเลิกและการหมดเวลาจะถูกล้าง
const controller = new AbortController();

document.querySelector("button.cancel").addEventListener("click", () => controller.abort());

fetchTimeout("example.json", 5000, { signal: controller.signal })
    .then(response => response.json())
    .then(console.log)
    .catch(error => {
        if (error.name === "AbortError") {
            // fetch aborted either due to timeout or due to user clicking the cancel button
        } else {
            // network error or json parsing error
        }
    });

หวังว่าจะช่วยได้


9

ยังไม่มีการสนับสนุนการหมดเวลาในการดึงข้อมูล API แต่สามารถทำได้โดยการห่อไว้ในสัญญา

สำหรับเช่น

  function fetchWrapper(url, options, timeout) {
    return new Promise((resolve, reject) => {
      fetch(url, options).then(resolve, reject);

      if (timeout) {
        const e = new Error("Connection timed out");
        setTimeout(reject, timeout, e);
      }
    });
  }

ฉันชอบอันนี้ดีกว่าไม่ซ้ำซากที่จะใช้มากกว่าหนึ่งครั้ง
dandavis

1
คำขอไม่ได้ถูกยกเลิกหลังจากหมดเวลาที่นี่ใช่ไหม ซึ่งอาจใช้ได้ดีสำหรับ OP แต่บางครั้งคุณต้องการยกเลิกคำขอฝั่งไคลเอ็นต์
trysis

2
@trysis ดีใช่. เมื่อเร็ว ๆ นี้ได้ใช้วิธีแก้ปัญหาสำหรับการยกเลิกการดึงข้อมูลด้วยAbortControllerแต่ยังอยู่ในช่วงทดลองด้วยการรองรับเบราว์เซอร์ที่ จำกัด Discussion
code-jaff

มันตลกดี IE & Edge เป็นคนเดียวที่รองรับ! เว้นแต่ว่าไซต์ Mozilla บนอุปกรณ์เคลื่อนที่จะทำงานอีกครั้ง ...
trysis

Firefox สนับสนุนมาตั้งแต่ปี 57 :: ดูที่ Chrome ::
Franklin Yu

7

แก้ไข : คำขอดึงข้อมูลจะยังคงทำงานอยู่เบื้องหลังและมักจะบันทึกข้อผิดพลาดในคอนโซลของคุณ

แน่นอนPromise.raceแนวทางดีกว่า

ดูลิงค์นี้สำหรับข้อมูลอ้างอิงPromise.race ()

การแข่งขันหมายความว่าสัญญาทั้งหมดจะดำเนินไปพร้อมกันและการแข่งขันจะหยุดลงทันทีที่คำสัญญาใดคำหนึ่งส่งคืนค่า ดังนั้นจะส่งกลับเพียงค่าเดียว คุณยังสามารถส่งผ่านฟังก์ชันเพื่อโทรได้หากการดึงข้อมูลหมดเวลา

fetchWithTimeout(url, {
  method: 'POST',
  body: formData,
  credentials: 'include',
}, 5000, () => { /* do stuff here */ });

หากสิ่งนี้ทำให้คุณสนใจการใช้งานที่เป็นไปได้คือ:

function fetchWithTimeout(url, options, delay, onTimeout) {
  const timer = new Promise((resolve) => {
    setTimeout(resolve, delay, {
      timeout: true,
    });
  });
  return Promise.race([
    fetch(url, options),
    timer
  ]).then(response => {
    if (response.timeout) {
      onTimeout();
    }
    return response;
  });
}

2

คุณสามารถสร้าง TimeoutPromise wrapper

function timeoutPromise(timeout, err, promise) {
  return new Promise(function(resolve,reject) {
    promise.then(resolve,reject);
    setTimeout(reject.bind(null,err), timeout);
  });
}

จากนั้นคุณสามารถห่อสัญญาใด ๆ

timeoutPromise(100, new Error('Timed Out!'), fetch(...))
  .then(...)
  .catch(...)  

จะไม่ยกเลิกการเชื่อมต่อพื้นฐาน แต่จะช่วยให้คุณหมดเวลาตามสัญญา
ข้อมูลอ้างอิง


2

หากคุณไม่ได้กำหนดค่าการหมดเวลาในรหัสของคุณจะเป็นการหมดเวลาคำขอเริ่มต้นของเบราว์เซอร์ของคุณ

1) Firefox - 90 วินาที

พิมพ์about:configช่อง URL ของ Firefox ค้นหาค่าที่ตรงกับคีย์network.http.connection-timeout

2) Chrome - 300 วินาที

ที่มา



0

การใช้c-Promise2 lib การดึงข้อมูลที่ยกเลิกได้ด้วยการหมดเวลาอาจมีลักษณะเช่นนี้ ( การสาธิต jsfiddle สด ):

import CPromise from "c-promise2"; // npm package

function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
    return new CPromise((resolve, reject, {signal}) => {
        fetch(url, {...fetchOptions, signal}).then(resolve, reject)
    }, timeout)
}
        
const chain = fetchWithTimeout("https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=10s", {timeout: 5000})
    .then(request=> console.log('done'));
    
// chain.cancel(); - to abort the request before the timeout

รหัสนี้เป็นแพ็คเกจ npm cp-fetch

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.