From 3dac33ffbaecff281a46b6d562a2c69f086ff33c Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Mon, 30 Nov 2020 10:59:21 +0100 Subject: [PATCH] Add backtraces to errors (#1498) Error handling has been reworked to always go through the new `error_template`, `error_json` and `error_atom` macros. They all accept a status code followed by a string message or an exception object. `error_json` accepts a hash with additional fields as third argument. If the second argument is an exception a backtrace will be printed, if it is a string only the string is printed. Since up till now only the exception message was printed a new `InfoException` class was added for situations where no backtrace is intended but a string cannot be used. `error_template` with a string message automatically localizes the message. Missing error translations have been collected in https://github.com/iv-org/invidious/issues/1497 `error_json` with a string message does not localize the message. This is the same as previous behavior. If translations are desired for `error_json` they can be added easily but those error messages have not been collected yet. Uncaught exceptions previously only printed a generic message ("Looks like you've found a bug in Invidious. [...]"). They still print that message but now also include a backtrace. --- src/invidious.cr | 390 ++++++++-------------------- src/invidious/channels.cr | 26 +- src/invidious/comments.cr | 4 +- src/invidious/helpers/errors.cr | 90 +++++++ src/invidious/helpers/tokens.cr | 12 +- src/invidious/mixes.cr | 2 +- src/invidious/playlists.cr | 10 +- src/invidious/routes/embed/index.cr | 4 +- src/invidious/routes/embed/show.cr | 11 +- src/invidious/routes/playlists.cr | 71 ++--- src/invidious/routes/watch.cr | 8 +- 11 files changed, 250 insertions(+), 378 deletions(-) create mode 100644 src/invidious/helpers/errors.cr diff --git a/src/invidious.cr b/src/invidious.cr index a9a3a963..9c3dffb2 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -367,9 +367,7 @@ get "/search" do |env| begin search_query, count, videos = process_search_query(query, page, user, region: nil) rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" + next error_template(500, ex) end env.set "search", query @@ -387,9 +385,7 @@ get "/login" do |env| end if !config.login_enabled - error_message = "Login has been disabled by administrator." - env.response.status_code = 400 - next templated "error" + next error_template(400, "Login has been disabled by administrator.") end referer = get_referer(env, "/feed/subscriptions") @@ -416,9 +412,7 @@ post "/login" do |env| referer = get_referer(env, "/feed/subscriptions") if !config.login_enabled - error_message = "Login has been disabled by administrator." - env.response.status_code = 403 - next templated "error" + next error_template(403, "Login has been disabled by administrator.") end # https://stackoverflow.com/a/574698 @@ -499,9 +493,7 @@ post "/login" do |env| headers["Cookie"] = URI.decode_www_form(headers["Cookie"]) if challenge_results[0][3]?.try &.== 7 - error_message = translate(locale, "Account has temporarily been disabled") - env.response.status_code = 423 - next templated "error" + next error_template(423, "Account has temporarily been disabled") end if token = challenge_results[0][-1]?.try &.[-1]?.try &.as_h?.try &.["5001"]?.try &.[-1].as_a?.try &.[-1].as_s @@ -515,9 +507,7 @@ post "/login" do |env| end if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED" - error_message = translate(locale, "Incorrect password") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Incorrect password") end prompt_type = challenge_results[0][-1]?.try &.[0].as_a?.try &.[0][2]? @@ -550,9 +540,7 @@ post "/login" do |env| end if tfa[5] == "QUOTA_EXCEEDED" - error_message = translate(locale, "Quota exceeded, try again in a few hours") - env.response.status_code = 423 - next templated "error" + next error_template(423, "Quota exceeded, try again in a few hours") end if !tfa_code @@ -608,9 +596,7 @@ post "/login" do |env| }, }.to_json else - error_message = translate(locale, "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.") - env.response.status_code = 500 - next templated "error" + next error_template(500, "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.") end traceback << "Submitting challenge..." @@ -621,9 +607,7 @@ post "/login" do |env| if (challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED") || (challenge_results[0][-1]?.try &.[5] == "INVALID_INPUT") - error_message = translate(locale, "Invalid TFA code") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Invalid TFA code") end traceback << "done.
" @@ -702,29 +686,22 @@ post "/login" do |env| traceback.rewind # error_message = translate(locale, "Login failed. This may be because two-factor authentication is not turned on for your account.") error_message = %(#{ex.message}
Traceback:
#{traceback.gets_to_end}
) - env.response.status_code = 500 - next templated "error" + next error_template(500, error_message) end when "invidious" if !email - error_message = translate(locale, "User ID is a required field") - env.response.status_code = 401 - next templated "error" + next error_template(401, "User ID is a required field") end if !password - error_message = translate(locale, "Password is a required field") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Password is a required field") end user = PG_DB.query_one?("SELECT * FROM users WHERE email = $1", email, as: User) if user if !user.password - error_message = translate(locale, "Please sign in using 'Log in with Google'") - env.response.status_code = 400 - next templated "error" + next error_template(400, "Please sign in using 'Log in with Google'") end if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) @@ -745,9 +722,7 @@ post "/login" do |env| secure: secure, http_only: true) end else - error_message = translate(locale, "Wrong username or password") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Wrong username or password") end # Since this user has already registered, we don't want to overwrite their preferences @@ -758,22 +733,16 @@ post "/login" do |env| end else if !config.registration_enabled - error_message = "Registration has been disabled by administrator." - env.response.status_code = 400 - next templated "error" + next error_template(400, "Registration has been disabled by administrator.") end if password.empty? - error_message = translate(locale, "Password cannot be empty") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Password cannot be empty") end # See https://security.stackexchange.com/a/39851 if password.bytesize > 55 - error_message = translate(locale, "Password should not be longer than 55 characters") - env.response.status_code = 400 - next templated "error" + next error_template(400, "Password cannot be longer than 55 characters") end password = password.byte_slice(0, 55) @@ -815,28 +784,28 @@ post "/login" do |env| begin validate_request(tokens[0], answer, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) end else # "text" answer = Digest::MD5.hexdigest(answer.downcase.strip) - found_valid_captcha = false + if tokens.empty? + next error_template(500, "Erroneous CAPTCHA") + end - error_message = translate(locale, "Erroneous CAPTCHA") + found_valid_captcha = false + error_exception = Exception.new tokens.each_with_index do |token, i| begin validate_request(token, answer, env.request, HMAC_KEY, PG_DB, locale) found_valid_captcha = true rescue ex - error_message = ex.message + error_exception = ex end end if !found_valid_captcha - env.response.status_code = 500 - next templated "error" + next error_template(500, error_exception) end end end @@ -902,9 +871,7 @@ post "/signout" do |env| begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) end PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", sid) @@ -1182,9 +1149,7 @@ post "/watch_ajax" do |env| if redirect next env.redirect referer else - error_message = {"error" => "No such user"}.to_json - env.response.status_code = 403 - next error_message + next error_json(403, "No such user") end end @@ -1201,13 +1166,10 @@ post "/watch_ajax" do |env| begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - env.response.status_code = 400 if redirect - error_message = ex.message - next templated "error" + next error_template(400, ex) else - error_message = {"error" => ex.message}.to_json - next error_message + next error_json(400, ex) end end @@ -1227,9 +1189,7 @@ post "/watch_ajax" do |env| when "action_mark_unwatched" PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email) else - error_message = {"error" => "Unsupported action #{action}"}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Unsupported action #{action}") end if redirect @@ -1259,9 +1219,7 @@ get "/modify_notifications" do |env| if redirect next env.redirect referer else - error_message = {"error" => "No such user"}.to_json - env.response.status_code = 403 - next error_message + next error_json(403, "No such user") end end @@ -1334,9 +1292,7 @@ post "/subscription_ajax" do |env| if redirect next env.redirect referer else - error_message = {"error" => "No such user"}.to_json - env.response.status_code = 403 - next error_message + next error_json(403, "No such user") end end @@ -1348,13 +1304,9 @@ post "/subscription_ajax" do |env| validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex if redirect - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) else - error_message = {"error" => ex.message}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, ex) end end @@ -1384,9 +1336,7 @@ post "/subscription_ajax" do |env| when "action_remove_subscriptions" PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", channel_id, email) else - error_message = {"error" => "Unsupported action #{action}"}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Unsupported action #{action}") end if redirect @@ -1569,7 +1519,7 @@ post "/data_control" do |env| PG_DB.exec("UPDATE playlists SET description = $1 WHERE id = $2", description, playlist.id) videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| - raise "Playlist cannot have more than 500 videos" if idx > 500 + raise InfoException.new("Playlist cannot have more than 500 videos") if idx > 500 video_id = video_id.try &.as_s? next if !video_id @@ -1706,51 +1656,37 @@ post "/change_password" do |env| # We don't store passwords for Google accounts if !user.password - error_message = "Cannot change password for Google accounts" - env.response.status_code = 400 - next templated "error" + next error_template(400, "Cannot change password for Google accounts") end begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) end password = env.params.body["password"]? if !password - error_message = translate(locale, "Password is a required field") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Password is a required field") end new_passwords = env.params.body.select { |k, v| k.match(/^new_password\[\d+\]$/) }.map { |k, v| v } if new_passwords.size <= 1 || new_passwords.uniq.size != 1 - error_message = translate(locale, "New passwords must match") - env.response.status_code = 400 - next templated "error" + next error_template(400, "New passwords must match") end new_password = new_passwords.uniq[0] if new_password.empty? - error_message = translate(locale, "Password cannot be empty") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Password cannot be empty") end if new_password.bytesize > 55 - error_message = translate(locale, "Password should not be longer than 55 characters") - env.response.status_code = 400 - next templated "error" + next error_template(400, "Password cannot be longer than 55 characters") end if !Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) - error_message = translate(locale, "Incorrect password") - env.response.status_code = 401 - next templated "error" + next error_template(401, "Incorrect password") end new_password = Crypto::Bcrypt::Password.create(new_password, cost: 10) @@ -1795,9 +1731,7 @@ post "/delete_account" do |env| begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) end view_name = "subscriptions_#{sha256(user.email)}" @@ -1849,9 +1783,7 @@ post "/clear_watch_history" do |env| begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) end PG_DB.exec("UPDATE users SET watched = '{}' WHERE email = $1", user.email) @@ -1904,9 +1836,7 @@ post "/authorize_token" do |env| begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) end scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v } @@ -1969,9 +1899,7 @@ post "/token_ajax" do |env| if redirect next env.redirect referer else - error_message = {"error" => "No such user"}.to_json - env.response.status_code = 403 - next error_message + next error_json(403, "No such user") end end @@ -1983,13 +1911,9 @@ post "/token_ajax" do |env| validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex if redirect - error_message = ex.message - env.response.status_code = 400 - next templated "error" + next error_template(400, ex) else - error_message = {"error" => ex.message}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, ex) end end @@ -2006,9 +1930,7 @@ post "/token_ajax" do |env| when .starts_with? "action_revoke_token" PG_DB.exec("DELETE FROM session_ids * WHERE id = $1 AND email = $2", session, user.email) else - error_message = {"error" => "Unsupported action #{action}"}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Unsupported action #{action}") end if redirect @@ -2048,9 +1970,7 @@ get "/feed/trending" do |env| begin trending, plid = fetch_trending(trending_type, region, locale) rescue ex - error_message = "#{ex.message}" - env.response.status_code = 500 - next templated "error" + next error_template(500, ex) end templated "trending" @@ -2145,9 +2065,7 @@ get "/feed/channel/:ucid" do |env| rescue ex : ChannelRedirect next env.redirect env.request.resource.gsub(ucid, ex.channel_id) rescue ex - error_message = ex.message - env.response.status_code = 500 - next error_message + next error_atom(500, ex) end response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}") @@ -2558,9 +2476,7 @@ get "/channel/:ucid" do |env| rescue ex : ChannelRedirect next env.redirect env.request.resource.gsub(ucid, ex.channel_id) rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" + next error_template(500, ex) end if channel.auto_generated @@ -2627,9 +2543,7 @@ get "/channel/:ucid/playlists" do |env| rescue ex : ChannelRedirect next env.redirect env.request.resource.gsub(ucid, ex.channel_id) rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" + next error_template(500, ex) end if channel.auto_generated @@ -2667,9 +2581,7 @@ get "/channel/:ucid/community" do |env| rescue ex : ChannelRedirect next env.redirect env.request.resource.gsub(ucid, ex.channel_id) rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" + next error_template(500, ex) end if !channel.tabs.includes? "community" @@ -2678,9 +2590,11 @@ get "/channel/:ucid/community" do |env| begin items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode)) - rescue ex + rescue ex : InfoException env.response.status_code = 500 error_message = ex.message + rescue ex + next error_template(500, ex) end env.set "search", "channel:#{channel.ucid} " @@ -2690,12 +2604,11 @@ end # API Endpoints get "/api/v1/stats" do |env| + locale = LOCALES[env.get("preferences").as(Preferences).locale]? env.response.content_type = "application/json" if !config.statistics_enabled - error_message = {"error" => "Statistics are not enabled."}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Statistics are not enabled.") end Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json @@ -2715,10 +2628,8 @@ get "/api/v1/storyboards/:id" do |env| begin video = get_video(id, PG_DB, region: region) rescue ex : VideoRedirect - error_message = {"error" => "Video is unavailable", "videoId" => ex.video_id}.to_json - env.response.status_code = 302 env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) - next error_message + next error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) rescue ex env.response.status_code = 500 next @@ -2803,10 +2714,8 @@ get "/api/v1/captions/:id" do |env| begin video = get_video(id, PG_DB, region: region) rescue ex : VideoRedirect - error_message = {"error" => "Video is unavailable", "videoId" => ex.video_id}.to_json - env.response.status_code = 302 env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) - next error_message + next error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) rescue ex env.response.status_code = 500 next @@ -2938,9 +2847,7 @@ get "/api/v1/comments/:id" do |env| begin comments = fetch_youtube_comments(id, PG_DB, continuation, format, locale, thin_mode, region, sort_by: sort_by) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end next comments @@ -2983,13 +2890,7 @@ end get "/api/v1/insights/:id" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - id = env.params.url["id"] - env.response.content_type = "application/json" - - error_message = {"error" => "YouTube has removed publicly available analytics."}.to_json - env.response.status_code = 410 - error_message + next error_json(410, "YouTube has removed publicly available analytics.") end get "/api/v1/annotations/:id" do |env| @@ -3078,14 +2979,10 @@ get "/api/v1/videos/:id" do |env| begin video = get_video(id, PG_DB, region: region) rescue ex : VideoRedirect - error_message = {"error" => "Video is unavailable", "videoId" => ex.video_id}.to_json - env.response.status_code = 302 env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) - next error_message + next error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end video.to_json(locale) @@ -3102,9 +2999,7 @@ get "/api/v1/trending" do |env| begin trending, plid = fetch_trending(trending_type, region, locale) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end videos = JSON.build do |json| @@ -3151,14 +3046,10 @@ get "/api/v1/channels/:ucid" do |env| begin channel = get_about_info(ucid, locale) rescue ex : ChannelRedirect - error_message = {"error" => "Channel is unavailable", "authorId" => ex.channel_id}.to_json - env.response.status_code = 302 env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) - next error_message + next error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end page = 1 @@ -3169,9 +3060,7 @@ get "/api/v1/channels/:ucid" do |env| begin count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end end @@ -3286,22 +3175,16 @@ end begin channel = get_about_info(ucid, locale) rescue ex : ChannelRedirect - error_message = {"error" => "Channel is unavailable", "authorId" => ex.channel_id}.to_json - env.response.status_code = 302 env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) - next error_message + next error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end begin count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end JSON.build do |json| @@ -3325,9 +3208,7 @@ end begin videos = get_latest_videos(ucid) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end JSON.build do |json| @@ -3355,14 +3236,10 @@ end begin channel = get_about_info(ucid, locale) rescue ex : ChannelRedirect - error_message = {"error" => "Channel is unavailable", "authorId" => ex.channel_id}.to_json - env.response.status_code = 302 env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) - next error_message + next error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) @@ -3403,9 +3280,7 @@ end begin fetch_channel_community(ucid, continuation, locale, format, thin_mode) rescue ex - env.response.status_code = 400 - error_message = {"error" => ex.message}.to_json - next error_message + next error_json(500, ex) end end end @@ -3463,9 +3338,7 @@ get "/api/v1/search" do |env| begin search_params = produce_search_params(sort_by, date, content_type, duration, features) rescue ex - env.response.status_code = 400 - error_message = {"error" => ex.message}.to_json - next error_message + next error_json(400, ex) end count, search_results = search(query, page, search_params, region).as(Tuple) @@ -3508,9 +3381,7 @@ get "/api/v1/search/suggestions" do |env| end end rescue ex - env.response.status_code = 500 - error_message = {"error" => ex.message}.to_json - next error_message + next error_json(500, ex) end end @@ -3537,16 +3408,12 @@ end begin playlist = get_playlist(PG_DB, plid, locale) rescue ex - env.response.status_code = 404 - error_message = {"error" => "Playlist does not exist."}.to_json - next error_message + next error_json(404, "Playlist does not exist.") end user = env.get?("user").try &.as(User) if !playlist || playlist.privacy.private? && playlist.author != user.try &.email - env.response.status_code = 404 - error_message = {"error" => "Playlist does not exist."}.to_json - next error_message + next error_json(404, "Playlist does not exist.") end response = playlist.to_json(offset, locale, continuation: continuation) @@ -3590,9 +3457,7 @@ get "/api/v1/mixes/:rdid" do |env| mix.videos = mix.videos[index..-1] rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end response = JSON.build do |json| @@ -3794,22 +3659,16 @@ post "/api/v1/auth/playlists" do |env| title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150) if !title - error_message = {"error" => "Invalid title."}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Invalid title.") end privacy = env.params.json["privacy"]?.try { |privacy| PlaylistPrivacy.parse(privacy.as(String).downcase) } if !privacy - error_message = {"error" => "Invalid privacy setting."}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Invalid privacy setting.") end if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 - error_message = {"error" => "User cannot have more than 100 playlists."}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "User cannot have more than 100 playlists.") end playlist = create_playlist(PG_DB, title, privacy, user) @@ -3831,15 +3690,11 @@ patch "/api/v1/auth/playlists/:plid" do |env| playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if !playlist || playlist.author != user.email && playlist.privacy.private? - env.response.status_code = 404 - error_message = {"error" => "Playlist does not exist."}.to_json - next error_message + next error_json(404, "Playlist does not exist.") end if playlist.author != user.email - env.response.status_code = 403 - error_message = {"error" => "Invalid user"}.to_json - next error_message + next error_json(403, "Invalid user") end title = env.params.json["title"].try &.as(String).delete("<>").byte_slice(0, 150) || playlist.title @@ -3859,6 +3714,8 @@ patch "/api/v1/auth/playlists/:plid" do |env| end delete "/api/v1/auth/playlists/:plid" do |env| + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + env.response.content_type = "application/json" user = env.get("user").as(User) @@ -3866,15 +3723,11 @@ delete "/api/v1/auth/playlists/:plid" do |env| playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if !playlist || playlist.author != user.email && playlist.privacy.private? - env.response.status_code = 404 - error_message = {"error" => "Playlist does not exist."}.to_json - next error_message + next error_json(404, "Playlist does not exist.") end if playlist.author != user.email - env.response.status_code = 403 - error_message = {"error" => "Invalid user"}.to_json - next error_message + next error_json(403, "Invalid user") end PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid) @@ -3893,36 +3746,26 @@ post "/api/v1/auth/playlists/:plid/videos" do |env| playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if !playlist || playlist.author != user.email && playlist.privacy.private? - env.response.status_code = 404 - error_message = {"error" => "Playlist does not exist."}.to_json - next error_message + next error_json(404, "Playlist does not exist.") end if playlist.author != user.email - env.response.status_code = 403 - error_message = {"error" => "Invalid user"}.to_json - next error_message + next error_json(403, "Invalid user") end if playlist.index.size >= 500 - env.response.status_code = 400 - error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json - next error_message + next error_json(400, "Playlist cannot have more than 500 videos") end video_id = env.params.json["videoId"].try &.as(String) if !video_id - env.response.status_code = 403 - error_message = {"error" => "Invalid videoId"}.to_json - next error_message + next error_json(403, "Invalid videoId") end begin video = get_video(video_id, PG_DB) rescue ex - error_message = {"error" => ex.message}.to_json - env.response.status_code = 500 - next error_message + next error_json(500, ex) end playlist_video = PlaylistVideo.new({ @@ -3949,6 +3792,8 @@ post "/api/v1/auth/playlists/:plid/videos" do |env| end delete "/api/v1/auth/playlists/:plid/videos/:index" do |env| + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + env.response.content_type = "application/json" user = env.get("user").as(User) @@ -3957,21 +3802,15 @@ delete "/api/v1/auth/playlists/:plid/videos/:index" do |env| playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if !playlist || playlist.author != user.email && playlist.privacy.private? - env.response.status_code = 404 - error_message = {"error" => "Playlist does not exist."}.to_json - next error_message + next error_json(404, "Playlist does not exist.") end if playlist.author != user.email - env.response.status_code = 403 - error_message = {"error" => "Invalid user"}.to_json - next error_message + next error_json(403, "Invalid user") end if !playlist.index.includes? index - env.response.status_code = 404 - error_message = {"error" => "Playlist does not contain index"}.to_json - next error_message + next error_json(404, "Playlist does not contain index") end PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index) @@ -4017,9 +3856,7 @@ post "/api/v1/auth/tokens/register" do |env| callback_url = env.params.json["callbackUrl"]?.try &.as(String) expire = env.params.json["expire"]?.try &.as(Int64) else - error_message = {"error" => "Invalid or missing header 'Content-Type'"}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Invalid or missing header 'Content-Type'") end if callback_url && callback_url.empty? @@ -4069,6 +3906,7 @@ post "/api/v1/auth/tokens/register" do |env| end post "/api/v1/auth/tokens/unregister" do |env| + locale = LOCALES[env.get("preferences").as(Preferences).locale]? env.response.content_type = "application/json" user = env.get("user").as(User) scopes = env.get("scopes").as(Array(String)) @@ -4082,9 +3920,7 @@ post "/api/v1/auth/tokens/unregister" do |env| elsif scopes_include_scope(scopes, "GET:tokens") PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", session) else - error_message = {"error" => "Cannot revoke session #{session}"}.to_json - env.response.status_code = 400 - next error_message + next error_json(400, "Cannot revoke session #{session}") end env.response.status_code = 204 @@ -4408,6 +4244,7 @@ get "/videoplayback/*" do |env| end get "/videoplayback" do |env| + locale = LOCALES[env.get("preferences").as(Preferences).locale]? query_params = env.params.query fvip = query_params["fvip"]? || "3" @@ -4474,9 +4311,7 @@ get "/videoplayback" do |env| if url.includes? "&file=seg.ts" if CONFIG.disabled?("livestreams") - env.response.status_code = 403 - error_message = "Administrator has disabled this endpoint." - next templated "error" + next error_template(403, "Administrator has disabled this endpoint.") end begin @@ -4508,9 +4343,7 @@ get "/videoplayback" do |env| else if query_params["title"]? && CONFIG.disabled?("downloads") || CONFIG.disabled?("dash") - env.response.status_code = 403 - error_message = "Administrator has disabled this endpoint." - next templated "error" + next error_template(403, "Administrator has disabled this endpoint.") end content_length = nil @@ -4851,14 +4684,9 @@ error 404 do |env| halt env, status_code: 302 end -error 500 do |env| - error_message = <<-END_HTML - Looks like you've found a bug in Invidious. Feel free to open a new issue - here - or send an email to - #{CONFIG.admin_email}. - END_HTML - templated "error" +error 500 do |env, ex| + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + error_template(500, ex) end static_headers do |response, filepath, filestat| diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 392c44ee..82921ac7 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -208,7 +208,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) author = rss.xpath_node(%q(//feed/title)) if !author - raise translate(locale, "Deleted or invalid channel") + raise InfoException.new("Deleted or invalid channel") end author = author.content @@ -226,13 +226,14 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) videos = [] of SearchVideo begin initial_data = JSON.parse(response.body).as_a.find &.["response"]? - raise "Could not extract JSON" if !initial_data + raise InfoException.new("Could not extract channel JSON") if !initial_data videos = extract_videos(initial_data.as_h, author, ucid) rescue ex if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") || response.body.includes?("https://www.google.com/sorry/index") - raise "Could not extract channel info. Instance is likely blocked." + raise InfoException.new("Could not extract channel info. Instance is likely blocked.") end + raise ex end rss.xpath_nodes("//feed/entry").each do |entry| @@ -287,7 +288,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) loop do response = get_channel_videos_response(ucid, page, auto_generated: auto_generated) initial_data = JSON.parse(response.body).as_a.find &.["response"]? - raise "Could not extract JSON" if !initial_data + raise InfoException.new("Could not extract channel JSON") if !initial_data videos = extract_videos(initial_data.as_h, author, ucid) count = videos.size @@ -507,8 +508,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end if response.status_code != 200 - error_message = translate(locale, "This channel does not exist.") - raise error_message + raise InfoException.new("This channel does not exist.") end ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"] @@ -518,7 +518,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) body = initial_data["contents"]?.try &.["twoColumnBrowseResultsRenderer"]["tabs"].as_a.select { |tab| tab["tabRenderer"]?.try &.["selected"].as_bool.== true }[0]? if !body - raise "Could not extract community tab." + raise InfoException.new("Could not extract community tab.") end body = body["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"] @@ -540,7 +540,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) body["response"]["continuationContents"]["backstageCommentsContinuation"]? if !body - raise "Could not extract continuation." + raise InfoException.new("Could not extract continuation.") end end @@ -551,7 +551,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) error_message = (message["text"]["simpleText"]? || message["text"]["runs"]?.try &.[0]?.try &.["text"]?) .try &.as_s || "" - raise error_message + raise InfoException.new(error_message) end response = JSON.build do |json| @@ -786,21 +786,19 @@ def get_about_info(ucid, locale) end if result.status_code != 200 - error_message = translate(locale, "This channel does not exist.") - raise error_message + raise InfoException.new("This channel does not exist.") end about = XML.parse_html(result.body) if about.xpath_node(%q(//div[contains(@class, "channel-empty-message")])) - error_message = translate(locale, "This channel does not exist.") - raise error_message + raise InfoException.new("This channel does not exist.") end initdata = extract_initial_data(result.body) if initdata.empty? error_message = about.xpath_node(%q(//div[@class="yt-alert-content"])).try &.content.strip error_message ||= translate(locale, "Could not get channel info.") - raise error_message + raise InfoException.new(error_message) end author = about.xpath_node(%q(//meta[@name="title"])).not_nil!["content"] diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 407cef78..ec203c41 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -92,7 +92,7 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so response = JSON.parse(response.body) if !response["response"]["continuationContents"]? - raise translate(locale, "Could not fetch comments") + raise InfoException.new("Could not fetch comments") end response = response["response"]["continuationContents"] @@ -266,7 +266,7 @@ def fetch_reddit_comments(id, sort_by = "confidence") thread = result[0].data.as(RedditListing).children[0].data.as(RedditLink) else - raise "Got error code #{search_results.status_code}" + raise InfoException.new("Could not fetch comments") end comments = result[1].data.as(RedditListing).children diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr new file mode 100644 index 00000000..927a398f --- /dev/null +++ b/src/invidious/helpers/errors.cr @@ -0,0 +1,90 @@ +# InfoExceptions are for displaying information to the user. +# +# An InfoException might or might not indicate that something went wrong. +# Historically Invidious didn't differentiate between these two options, so to +# maintain previous functionality InfoExceptions do not print backtraces. +class InfoException < Exception +end + +macro error_template(*args) + error_template_helper(env, config, locale, {{*args}}) +end + +def error_template_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) + if exception.is_a?(InfoException) + return error_template_helper(env, config, locale, status_code, exception.message || "") + end + env.response.status_code = status_code + error_message = <<-END_HTML + Looks like you've found a bug in Invidious. Feel free to open a new issue + here + or send an email to + #{CONFIG.admin_email}. +
+
+
+ Please include the following text in your message: +
#{exception.inspect_with_backtrace}
+ END_HTML + return templated "error" +end + +def error_template_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) + env.response.status_code = status_code + error_message = translate(locale, message) + return templated "error" +end + +macro error_atom(*args) + error_atom_helper(env, config, locale, {{*args}}) +end + +def error_atom_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) + if exception.is_a?(InfoException) + return error_atom_helper(env, config, locale, status_code, exception.message || "") + end + env.response.content_type = "application/atom+xml" + env.response.status_code = status_code + return "#{exception.inspect_with_backtrace}" +end + +def error_atom_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) + env.response.content_type = "application/atom+xml" + env.response.status_code = status_code + return "#{message}" +end + +macro error_json(*args) + error_json_helper(env, config, locale, {{*args}}) +end + +def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception, additional_fields : Hash(String, Object) | Nil) + if exception.is_a?(InfoException) + return error_json_helper(env, config, locale, status_code, exception.message || "", additional_fields) + end + env.response.content_type = "application/json" + env.response.status_code = status_code + error_message = {"error" => exception.message, "errorBacktrace" => exception.inspect_with_backtrace} + if additional_fields + error_message = error_message.merge(additional_fields) + end + return error_message.to_json +end + +def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) + return error_json_helper(env, config, locale, status_code, exception, nil) +end + +def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String, additional_fields : Hash(String, Object) | Nil) + env.response.content_type = "application/json" + env.response.status_code = status_code + error_message = {"error" => message} + if additional_fields + error_message = error_message.merge(additional_fields) + end + return error_message.to_json +end + +def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) + error_json_helper(env, config, locale, status_code, message, nil) +end diff --git a/src/invidious/helpers/tokens.cr b/src/invidious/helpers/tokens.cr index 39aae367..a09ce90b 100644 --- a/src/invidious/helpers/tokens.cr +++ b/src/invidious/helpers/tokens.cr @@ -70,33 +70,33 @@ def validate_request(token, session, request, key, db, locale = nil) when JSON::Any token = token.as_h when Nil - raise translate(locale, "Hidden field \"token\" is a required field") + raise InfoException.new("Hidden field \"token\" is a required field") end expire = token["expire"]?.try &.as_i if expire.try &.< Time.utc.to_unix - raise translate(locale, "Token is expired, please try again") + raise InfoException.new("Token is expired, please try again") end if token["session"] != session - raise translate(locale, "Erroneous token") + raise InfoException.new("Erroneous token") end scopes = token["scopes"].as_a.map { |v| v.as_s } scope = "#{request.method}:#{request.path.lchop("/api/v1/auth/").lstrip("/")}" if !scopes_include_scope(scopes, scope) - raise translate(locale, "Invalid scope") + raise InfoException.new("Invalid scope") end if !Crypto::Subtle.constant_time_compare(token["signature"].to_s, sign_token(key, token)) - raise translate(locale, "Invalid signature") + raise InfoException.new("Invalid signature") end if token["nonce"]? && (nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", token["nonce"], as: {String, Time})) if nonce[1] > Time.utc db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.utc(1990, 1, 1), nonce[0]) else - raise translate(locale, "Erroneous token") + raise InfoException.new("Erroneous token") end end diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr index c69eb0c4..55b01174 100644 --- a/src/invidious/mixes.cr +++ b/src/invidious/mixes.cr @@ -30,7 +30,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil) initial_data = extract_initial_data(response.body) if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]? - raise translate(locale, "Could not create mix.") + raise InfoException.new("Could not create mix.") end playlist = initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]["playlist"] diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index c984a12a..2389de49 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -338,7 +338,7 @@ def get_playlist(db, plid, locale, refresh = true, force_refresh = false) if playlist = db.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) return playlist else - raise "Playlist does not exist." + raise InfoException.new("Playlist does not exist.") end else return fetch_playlist(plid, locale) @@ -353,16 +353,16 @@ def fetch_playlist(plid, locale) response = YT_POOL.client &.get("/playlist?list=#{plid}&hl=en") if response.status_code != 200 if response.headers["location"]?.try &.includes? "/sorry/index" - raise "Could not extract playlist info. Instance is likely blocked." + raise InfoException.new("Could not extract playlist info. Instance is likely blocked.") else - raise translate(locale, "Not a playlist.") + raise InfoException.new("Not a playlist.") end end initial_data = extract_initial_data(response.body) playlist_info = initial_data["sidebar"]?.try &.["playlistSidebarRenderer"]?.try &.["items"]?.try &.[0]["playlistSidebarPrimaryInfoRenderer"]? - raise "Could not extract playlist info" if !playlist_info + raise InfoException.new("Could not extract playlist info") if !playlist_info title = playlist_info["title"]?.try &.["runs"][0]?.try &.["text"]?.try &.as_s || "" desc_item = playlist_info["description"]? @@ -390,7 +390,7 @@ def fetch_playlist(plid, locale) author_info = initial_data["sidebar"]?.try &.["playlistSidebarRenderer"]?.try &.["items"]?.try &.[1]["playlistSidebarSecondaryInfoRenderer"]? .try &.["videoOwner"]["videoOwnerRenderer"]? - raise "Could not extract author info" if !author_info + raise InfoException.new("Could not extract author info") if !author_info author_thumbnail = author_info["thumbnail"]["thumbnails"][0]["url"]?.try &.as_s || "" author = author_info["title"]["runs"][0]["text"]?.try &.as_s || "" diff --git a/src/invidious/routes/embed/index.cr b/src/invidious/routes/embed/index.cr index 79c91d86..32a4966b 100644 --- a/src/invidious/routes/embed/index.cr +++ b/src/invidious/routes/embed/index.cr @@ -8,9 +8,7 @@ class Invidious::Routes::Embed::Index < Invidious::Routes::BaseRoute offset = env.params.query["index"]?.try &.to_i? || 0 videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) rescue ex - error_message = ex.message - env.response.status_code = 500 - return templated "error" + return error_template(500, ex) end url = "/embed/#{videos[0].id}?#{env.params.query}" diff --git a/src/invidious/routes/embed/show.cr b/src/invidious/routes/embed/show.cr index 23c2b86f..8a655556 100644 --- a/src/invidious/routes/embed/show.cr +++ b/src/invidious/routes/embed/show.cr @@ -38,9 +38,7 @@ class Invidious::Routes::Embed::Show < Invidious::Routes::BaseRoute offset = env.params.query["index"]?.try &.to_i? || 0 videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) rescue ex - error_message = ex.message - env.response.status_code = 500 - return templated "error" + return error_template(500, ex) end url = "/embed/#{videos[0].id}" @@ -63,8 +61,7 @@ class Invidious::Routes::Embed::Show < Invidious::Routes::BaseRoute env.params.query.delete_all("channel") if !video_id || video_id == "live_stream" - error_message = "Video is unavailable." - return templated "error" + return error_template(500, "Video is unavailable.") end url = "/embed/#{video_id}" @@ -100,9 +97,7 @@ class Invidious::Routes::Embed::Show < Invidious::Routes::BaseRoute rescue ex : VideoRedirect return env.redirect env.request.resource.gsub(id, ex.video_id) rescue ex - error_message = ex.message - env.response.status_code = 500 - return templated "error" + return error_template(500, ex) end if preferences.annotations_subscribed && diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 52acf266..6c899054 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -56,26 +56,21 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - return templated "error" + return error_template(400, ex) end title = env.params.body["title"]?.try &.as(String) if !title || title.empty? - error_message = "Title cannot be empty." - return templated "error" + return error_template(400, "Title cannot be empty.") end privacy = PlaylistPrivacy.parse?(env.params.body["privacy"]?.try &.as(String) || "") if !privacy - error_message = "Invalid privacy setting." - return templated "error" + return error_template(400, "Invalid privacy setting.") end if PG_DB.query_one("SELECT count(*) FROM playlists WHERE author = $1", user.email, as: Int64) >= 100 - error_message = "User cannot have more than 100 playlists." - return templated "error" + return error_template(400, "User cannot have more than 100 playlists.") end playlist = create_playlist(PG_DB, title, privacy, user) @@ -142,9 +137,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - return templated "error" + return error_template(400, ex) end playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) @@ -217,9 +210,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute begin validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex - error_message = ex.message - env.response.status_code = 400 - return templated "error" + return error_template(400, ex) end playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) @@ -306,9 +297,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute if redirect return env.redirect referer else - error_message = {"error" => "No such user"}.to_json - env.response.status_code = 403 - return error_message + return error_json(403, "No such user") end end @@ -320,13 +309,9 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute validate_request(token, sid, env.request, HMAC_KEY, PG_DB, locale) rescue ex if redirect - error_message = ex.message - env.response.status_code = 400 - return templated "error" + return error_template(400, ex) else - error_message = {"error" => ex.message}.to_json - env.response.status_code = 400 - return error_message + return error_json(400, ex) end end @@ -353,13 +338,9 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute raise "Invalid user" if playlist.author != user.email rescue ex if redirect - error_message = ex.message - env.response.status_code = 400 - return templated "error" + return error_template(400, ex) else - error_message = {"error" => ex.message}.to_json - env.response.status_code = 400 - return error_message + return error_json(400, ex) end end @@ -374,13 +355,10 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute # TODO: Playlist stub when "action_add_video" if playlist.index.size >= 500 - env.response.status_code = 400 if redirect - error_message = "Playlist cannot have more than 500 videos" - return templated "error" + return error_template(400, "Playlist cannot have more than 500 videos") else - error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json - return error_message + return error_json(400, "Playlist cannot have more than 500 videos") end end @@ -389,13 +367,10 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute begin video = get_video(video_id, PG_DB) rescue ex - env.response.status_code = 500 if redirect - error_message = ex.message - return templated "error" + return error_template(500, ex) else - error_message = {"error" => ex.message}.to_json - return error_message + return error_json(500, ex) end end @@ -423,9 +398,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute when "action_move_video_before" # TODO: Playlist stub else - error_message = {"error" => "Unsupported action #{action}"}.to_json - env.response.status_code = 400 - return error_message + return error_json(400, "Unsupported action #{action}") end if redirect @@ -457,15 +430,11 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute begin playlist = get_playlist(PG_DB, plid, locale) rescue ex - error_message = ex.message - env.response.status_code = 500 - return templated "error" + return error_template(500, ex) end if playlist.privacy == PlaylistPrivacy::Private && playlist.author != user.try &.email - error_message = "This playlist is private." - env.response.status_code = 403 - return templated "error" + return error_template(403, "This playlist is private.") end begin @@ -495,9 +464,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute begin mix = fetch_mix(rdid, continuation, locale: locale) rescue ex - error_message = ex.message - env.response.status_code = 500 - return templated "error" + return error_template(500, ex) end templated "mix" diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 4eee7793..7018d0a4 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -12,9 +12,7 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute id = env.params.query["v"] if env.params.query["v"].empty? - error_message = "Invalid parameters." - env.response.status_code = 400 - return templated "error" + return error_template(400, "Invalid parameters.") end if id.size > 11 @@ -56,10 +54,8 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute rescue ex : VideoRedirect return env.redirect env.request.resource.gsub(id, ex.video_id) rescue ex - error_message = ex.message - env.response.status_code = 500 logger.puts("#{id} : #{ex.message}") - return templated "error" + return error_template(500, ex) end if preferences.annotations_subscribed &&