Search code examples
ubuntuserverfetchdeno

Deno unexpected internal error encountered


I make a lot of fetches through the fetch-API in Deno TypeScript. The problem now is that randomly I get the following error (can't be caught by try-catch):

error: Uncaught (in promise) TypeError: error sending request for url (https://www.googleapis.com/calendar/v3/calendars/****@group.calendar.google.com/events/?calendarId=****group.calendar.google.com): http2 error: stream error received: unexpected internal error encountered
        const result: Response = await fetch(url, {
                                 ^
    at async mainFetch (deno:ext/fetch/26_fetch.js:288:14)
    at async fetch (deno:ext/fetch/26_fetch.js:505:9)
    at async gFetchEvent (file:///home/****/my_script.ts:98:27)

And I don't have any clue how to fix it. Is this a Deno bug?

I have the following deno version installed:

deno 1.25.2 (release, x86_64-unknown-linux-gnu)
v8 10.6.194.5
typescript 4.7.4

There is no particular line of code that breaks my program, just after some time (could be minutes, could be days) my program crashes with this error.

It only appears on my Ubuntu 20.04.5 LTS vServer by 1blu with the following hardware specs:

H/W path  Device       Class      Description
=============================================
                       system     Computer
/0                     bus        Motherboard
/0/0                   memory     8GiB System memory
/0/1                   processor  AMD EPYC 7452 32-Core Processor
/1        veth09bb0e5  network    Ethernet interface
/2        veth0ab53b0  network    Ethernet interface
/3        veth62387d0  network    Ethernet interface
/4        veth7dbc5b2  network    Ethernet interface
/5        vethb66edc6  network    Ethernet interface

(output of sudo lshw -short)

The code in my main script:

try {
  await main()
} catch (e) {
  console.log(new Date(), e.stack)
  Deno.writeTextFileSync(`logs/${Date.now()}`, "" + e.stack)
}

my main function

// this program checks changes in my school schedule and automatically puts them in my google calendar
export default async function main() {
  await Kantplaner.init()
  while (true) {
    // MKDate is just a class that extends Date for more functionallity, nothing special
    const start_day = new MKDate(Date.now())

    // repeats 14 times for the next 14 days
    for (let i = 0; i < 14; i++) {
      const date: MKDate = i ? start_day.nextDate(1) : start_day
      
      // get my schedule from my school's site
      const vplan: VPlan | null = await Indiware(date)
      if (!vplan) continue
      
      // fetch the existing events with google calendar api and check if something in the meantime changed
      const calendar = await Kantplaner.list_events(date)
      // male one big object containing all indices that were previously built by `await Indiware(date)`
      const GrandIndex = { ...vplan.data.KlassenIndex, ...vplan.data.LehrerIndex }
      for (const item of calendar.items) {
        const stundenNr = "some_string"
        const stundenMitDerID = GrandIndex[stundenNr]
        // if the event is not in my school's schedule anymore, delete it
        if (!stundenMitDerID) {
          await Kantplaner.delete_event(item.id)
          continue
        }
        // for every other event check differences and update the corresponding Google event
        // `stundenMitDerID` is an array of events with the same id (can happen at my school)
        for (let i = 0; i < stundenMitDerID.length; ++i) {
          // ... create update (doesn't matter)
          const update: Kantplaner.Update = {}
          await Kantplaner.update_event(item.id, update)
          // remove lesson from index to avoid another creation
          GrandIndex[stundenNr].splice(i)
          if (GrandIndex[stundenNr].length == 0) delete GrandIndex[stundenNr]
        }
      }
      // create every remainig event
      for (const stundenNr in GrandIndex) {
        const stundenMitDerID = GrandIndex[stundenNr]
        for (let i = 0; i < stundenMitDerID.length; ++i) {
          await Kantplaner.create_event({
            // event data
          })
        }
      }
    }
    // wait one minute to reduce unnecessary fetches
    await new Promise(r => setTimeout(r, 60_000))
  }
}

All appearances of gFetchEvent:

export async function list_events(date: MKDate, TIME_SHIFT: string): Promise<Calendar> {
    const calendar: Calendar | null = await gFetchEvent("/", "GET", {
        timeMin: date.format(`yyyy-MM-ddT00:00:00${TIME_SHIFT}`),
        timeMax: date.format(`yyyy-MM-ddT23:59:59${TIME_SHIFT}`),
    })
    if (!calendar) return EMPTY_CALENDAR

    let nextPageToken = calendar.nextPageToken
    while (nextPageToken) {
        const nextPage: Calendar = await gFetchEvent("/", "GET", {
            pageToken: nextPageToken,
            timeMin: date.format(`yyyy-MM-ddT00:00:00${TIME_SHIFT}`),
            timeMax: date.format(`yyyy-MM-ddT23:59:59${TIME_SHIFT}`),
        })
        calendar.items.push(...nextPage.items)
        nextPageToken = nextPage.nextPageToken
    }
    return calendar
}

export async function create_event(event: Event) {
    await gFetchEvent("/", "POST", { calendarId: CALENDAR_ID }, event)
}

export async function update_event(eventId: string, update: Update) {
    await gFetchEvent(`/${eventId}`, "PATCH", {
        sendUpdates: "none"
    }, update)
}

export async function delete_event(eventId: string) {
    await gFetchEvent(`/${eventId}`, "DELETE", {
        calendarId: CALENDAR_ID,
        eventId: eventId,
        sendUpdates: "none"
    })
}

The code where I fetch:

async function gFetchEvent(urlPath: string, method: string, params?: { [key: string]: string }, body?: any) {
    if (!initiated) return null
    const url = new URL(CALENDAR_API_URL + urlPath)
    if (params) for (const key of Object.keys(params)) url.searchParams.append(key, params[key])

    const result: Response = await fetch(url, {
        headers: {
            Authorization: "Bearer " + access_token,
            Accept: "application/json",
            "Content-Type": "application/json"
        },
        method: method,
        body: JSON.stringify(body)
    })
    
    await new Promise(f => setTimeout(f, 100))
    if (result.ok) {
        const text = await result.text()
        if (text.length > 1) return JSON.parse(text)
        else return {}
    } else if (result.status == 403) {
        gFetchEvent(urlPath, method, params, body)
        return
    }
    return null
}

Every function call like list_events or update_event implicitly calls gFetchEvent function (with the different url's and ).


Solution

  • You're missing an await, without it, the error won't bubble correctly to the try/catch in your main function.

        if (result.ok) {
            const text = await result.text()
            if (text.length > 1) return JSON.parse(text)
            else return {}
        } else if (result.status == 403) {
            // You were missing an await here
            const res = await gFetchEvent(urlPath, method, params, body)
            return res;
            // or just
            // return gFetchEvent(urlPath, method, params, body);
        }
    

    Alternatively if you don't want to use await in that path, you can attach a .catch handler to gFetchEvent to prevent the Uncaught Promise error.