typescriptyoutube-apiyoutube-data-api

YouTube API Uploads stuck at 0%


When uploading videos to the YouTube service using the API, the API response indicated that the upload is successful. But on the website channel view the video is stuck at a 0% upload status (see screenshot). The percentage does not increase and the video never becomes viewable.

screenshot showing that video upload is stuck at 0%

I am using the typescript/javascript api client library and the upload code looks as follows:

import fs from 'fs';
import { google, Auth } from 'googleapis';


async function uploadYouTubeVideoWithAuth(
  auth: Auth.OAuth2Client,
  videoLocalLocation: string,
  title: string,
  description: string
): Promise<string> {
  google.options({ auth });
  const youtube = google.youtube('v3');

  const resp = await youtube.videos.insert({
    part: ['id', 'snippet', 'status'],
    requestBody: {
      snippet: {
        title,
        description,
      },
      status: {
        privacyStatus: 'private',
        selfDeclaredMadeForKids: false,
      },
    },
    media: {
      body: fs.createReadStream(videoLocalLocation),
    },
  });
  console.log(JSON.stringify(resp, null, 2));
  const videoId = resp.data.id;
  if (!videoId) {
    throw new Error('failed to get videoId of uploaded video');
  }
  return videoId;
}

The JSON response from the request looks as follows:

{
  "config": {
    "url": "https://youtube.googleapis.com/upload/youtube/v3/videos?part=id&part=snippet&part=status&uploadType=multipart",
    "method": "POST",
    "apiVersion": "",
    "userAgentDirectives": [
      {
        "product": "google-api-nodejs-client",
        "version": "7.2.0",
        "comment": "gzip"
      }
    ],
    "data": {
      "_events": {},
      "_readableState": {
        "highWaterMark": 16384,
        "buffer": [],
        "bufferIndex": 0,
        "length": 0,
        "pipes": [],
        "awaitDrainWriters": null
      },
      "_writableState": {
        "highWaterMark": 16384,
        "length": 0,
        "corked": 0,
        "writelen": 0,
        "bufferedIndex": 0,
        "pendingcb": 0
      },
      "allowHalfOpen": true,
      "_eventsCount": 2
    },
    "headers": {
      "x-goog-api-client": "gdcl/7.2.0 gl-node/20.18.3",
      "content-type": "multipart/related; boundary=a34455bc-c02c-4c89-896a-2b1b530bf92b",
      "Accept-Encoding": "gzip",
      "User-Agent": "google-api-nodejs-client/7.2.0 (gzip)",
      "Authorization": "Bearer <omit>"
    },
    "params": {
      "part": [
        "id",
        "snippet",
        "status"
      ],
      "uploadType": "multipart"
    },
    "retry": true,
    "body": {
      "_events": {},
      "_readableState": {
        "highWaterMark": 16384,
        "buffer": [],
        "bufferIndex": 0,
        "length": 0,
        "pipes": [],
        "awaitDrainWriters": null
      },
      "_writableState": {
        "highWaterMark": 16384,
        "length": 0,
        "corked": 0,
        "writelen": 0,
        "bufferedIndex": 0,
        "pendingcb": 0
      },
      "allowHalfOpen": true,
      "_eventsCount": 2
    },
    "responseType": "unknown"
  },
  "data": {
    "kind": "youtube#video",
    "etag": "yWFiu0ADCdhaQtb1HoXEH_pDoeA",
    "id": "YlDM_luRQLE",
    "snippet": {
      "publishedAt": "2025-04-10T15:53:11Z",
      "channelId": "<omitted>",
      "title": "<omitted>",
      "description": "<omitted>",
      "thumbnails": {
        "default": {
          "url": "https://i9.ytimg.com/vi/YlDM_luRQLE/default.jpg?sqp=CKjR378G&rs=AOn4CLCjq6jG8VlCvegcjsZPuGm7PgqTsw",
          "width": 120,
          "height": 90
        },
        "medium": {
          "url": "https://i9.ytimg.com/vi/YlDM_luRQLE/mqdefault.jpg?sqp=CKjR378G&rs=AOn4CLD564Jj03lC_HZyByhbW9fDSwCQPg",
          "width": 320,
          "height": 180
        },
        "high": {
          "url": "https://i9.ytimg.com/vi/YlDM_luRQLE/hqdefault.jpg?sqp=CKjR378G&rs=AOn4CLACHEqSkwjdlrUkujlTdJj7YNf9Vw",
          "width": 480,
          "height": 360
        }
      },
      "channelTitle": "<omitted_channel_name>",
      "categoryId": "22",
      "liveBroadcastContent": "none",
      "localized": {
        "title": "<omitted_title>",
        "description": "<omitted_description>"
      }
    },
    "status": {
      "uploadStatus": "uploaded",
      "privacyStatus": "private",
      "license": "youtube",
      "embeddable": true,
      "publicStatsViewable": true,
      "selfDeclaredMadeForKids": false
    }
  },
  "headers": {
    "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
    "content-encoding": "gzip",
    "content-type": "application/json; charset=UTF-8",
    "date": "Thu, 10 Apr 2025 15:53:13 GMT",
    "server": "UploadServer",
    "transfer-encoding": "chunked",
    "vary": "Origin, X-Origin, Referer",
    "warning": "214 UploadServer gzipped",
    "x-guploader-response-body-transformations": "gzipped",
    "x-guploader-uploadid": "AKDAyIurvH8jzOBh3gfTTXIYX_GAf11AgQjdT1kByGNdX6kShi-yEsjUYPn0LO27dYbGXLdP"
  },
  "status": 200,
  "statusText": "OK",
  "request": {
    "responseURL": "https://youtube.googleapis.com/upload/youtube/v3/videos?part=id&part=snippet&part=status&uploadType=multipart"
  }
}

What I expected to happen: Once the response is recieved from the API the video should appear fully uploaded on website channel view, but it is stuck at 0% uploaded and is not playable.


It's not 100% reproducible, the first time I tried uploading a video using the above code, it worked. Everytime since, it has not.


Further details:

I am using a refresh token to create the auth object:

function authorizeWithOAuth(
  refreshToken: string,
  credentials: ClientCredentials
): Auth.OAuth2Client {
  const clientSecret = credentials.client_secret;
  const clientId = credentials.client_id;
  const redirectUrl = credentials.redirect_uris[0];

  const oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
  oauth2Client.setCredentials({ refresh_token: refreshToken });

  return oauth2Client;
}

Linked google issue: https://issuetracker.google.com/issues/409959470


Solution

  • I have resolved my issue, it was my mistake. The file that was being uploaded had not been correctly downloaded from the origin source first. So i guess the code was attempting to upload a file containing 0 bytes or something similar.