Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes [1.3k weekly errors] [JS] Adds error handlers for PNPM exceptions #11316

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def initialize(dependencies:, dependency_files:, repo_contents_path:, credential
@dependency_files = dependency_files
@repo_contents_path = repo_contents_path
@credentials = credentials
@error_handler = PnpmErrorHandler.new(
dependencies: dependencies,
dependency_files: dependency_files
)
end

def updated_pnpm_lock_content(pnpm_lock)
Expand All @@ -36,6 +40,7 @@ def updated_pnpm_lock_content(pnpm_lock)
attr_reader :dependency_files
attr_reader :repo_contents_path
attr_reader :credentials
attr_reader :error_handler

IRRESOLVABLE_PACKAGE = "ERR_PNPM_NO_MATCHING_VERSION"
INVALID_REQUIREMENT = "ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER"
Expand All @@ -46,12 +51,12 @@ def updated_pnpm_lock_content(pnpm_lock)
UNAUTHORIZED_PACKAGE = /ERR_PNPM_FETCH_401[ [^:print:]]+GET (?<dependency_url>.*): Unauthorized - 401/

# ERR_PNPM_FETCH ERROR CODES
ERR_PNPM_FETCH_401 = /ERR_PNPM_FETCH_401.*GET (?<dependency_url>.*): - 401/
ERR_PNPM_FETCH_403 = /ERR_PNPM_FETCH_403.*GET (?<dependency_url>.*): - 403/
ERR_PNPM_FETCH_404 = /ERR_PNPM_FETCH_404.*GET (?<dependency_url>.*): - 404/
ERR_PNPM_FETCH_500 = /ERR_PNPM_FETCH_500.*GET (?<dependency_url>.*): - 500/
ERR_PNPM_FETCH_502 = /ERR_PNPM_FETCH_502.*GET (?<dependency_url>.*): - 502/
ERR_PNPM_FETCH_503 = /ERR_PNPM_FETCH_503.*GET (?<dependency_url>.*): - 503/
ERR_PNPM_FETCH_401 = /ERR_PNPM_FETCH_401.*GET (?<dependency_url>.*):/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should 401 throw a separate error i.e. private_registry_not_configured or not reachable? I think line 51 is already hinting at this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abdulapopoola , makes sense, will take care of it in next iteration.

ERR_PNPM_FETCH_403 = /ERR_PNPM_FETCH_403.*GET (?<dependency_url>.*):/
ERR_PNPM_FETCH_404 = /ERR_PNPM_FETCH_404.*GET (?<dependency_url>.*):/
ERR_PNPM_FETCH_500 = /ERR_PNPM_FETCH_500.*GET (?<dependency_url>.*):/
ERR_PNPM_FETCH_502 = /ERR_PNPM_FETCH_502.*GET (?<dependency_url>.*):/
ERR_PNPM_FETCH_503 = /ERR_PNPM_FETCH_503.*GET (?<dependency_url>.*):/

# ERR_PNPM_UNSUPPORTED_ENGINE
ERR_PNPM_UNSUPPORTED_ENGINE = /ERR_PNPM_UNSUPPORTED_ENGINE/
Expand Down Expand Up @@ -251,6 +256,8 @@ def handle_pnpm_lock_updater_error(error, pnpm_lock)
pnpm_lock)
end

error_handler.handle_pnpm_error(error)

raise
end
# rubocop:enable Metrics/AbcSize
Expand Down Expand Up @@ -360,5 +367,60 @@ def sanitize_message(message)
end
end
end

class PnpmErrorHandler
extend T::Sig

# remote connection closed
ECONNRESET_ERROR = /ECONNRESET/

# socket hang up error code
SOCKET_HANG_UP = /socket hang up/

# ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC error
ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC = /ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC/

# duplicate package error code
DUPLICATE_PACKAGE = /Found duplicates/

ERR_PNPM_NO_VERSIONS = /ERR_PNPM_NO_VERSIONS/

# Initializes the YarnErrorHandler with dependencies and dependency files
sig do
params(
dependencies: T::Array[Dependabot::Dependency],
dependency_files: T::Array[Dependabot::DependencyFile]
).void
end
def initialize(dependencies:, dependency_files:)
@dependencies = dependencies
@dependency_files = dependency_files
end

private

sig { returns(T::Array[Dependabot::Dependency]) }
attr_reader :dependencies

sig { returns(T::Array[Dependabot::DependencyFile]) }
attr_reader :dependency_files

public

# Handles errors with specific to yarn error codes
sig { params(error: SharedHelpers::HelperSubprocessFailed).void }
def handle_pnpm_error(error)
if error.message.match?(DUPLICATE_PACKAGE) || error.message.match?(ERR_PNPM_NO_VERSIONS) ||
error.message.match?(ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC)

raise DependencyFileNotResolvable, "Error resolving dependency"
end

## Clean error message from ANSI escape codes
return unless error.message.match?(ECONNRESET_ERROR) || error.message.match?(SOCKET_HANG_UP)

raise InconsistentRegistryResponse, "Inconsistent registry response while resolving dependency"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# typed: false
# frozen_string_literal: true

require "spec_helper"
require "dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater"
require "dependabot/npm_and_yarn/dependency_files_filterer"
require "dependabot/dependency"
require "dependabot/dependency_file"
require "dependabot/shared_helpers"
require "dependabot/errors"

RSpec.describe Dependabot::NpmAndYarn::PnpmErrorHandler do
subject(:error_handler) { described_class.new(dependencies: dependencies, dependency_files: dependency_files) }

let(:dependencies) { [dependency] }
let(:error) { instance_double(Dependabot::SharedHelpers::HelperSubprocessFailed, message: error_message) }

let(:dependency) do
Dependabot::Dependency.new(
name: dependency_name,
version: version,
requirements: [],
previous_requirements: [],
package_manager: "npm_and_yarn"
)
end
let(:dependency_files) { project_dependency_files("pnpm/git_dependency_local_file") }

let(:credentials) do
[Dependabot::Credential.new({
"type" => "git_source",
"host" => "github.com"
})]
end

let(:dependency_name) { "@segment/analytics.js-integration-facebook-pixel" }
let(:version) { "github:segmentio/analytics.js-integrations#2.4.1" }
let(:yarn_lock) do
dependency_files.find { |f| f.name == "pnpm.lock" }
end

describe "#initialize" do
it "initializes with dependencies and dependency files" do
expect(error_handler.send(:dependencies)).to eq(dependencies)
expect(error_handler.send(:dependency_files)).to eq(dependency_files)
end
end

describe "#handle_error" do
context "when the error message contains Inconsistent Registry Response" do
let(:error_message) do
"ECONNRESET  request to https://artifactory.schaeffler.com/as.zip failed, reason: socket hang up"
end

it "raises a InconsistentRegistryResponse error with the correct message" do
expect do
error_handler.handle_pnpm_error(error)
end.to raise_error(Dependabot::InconsistentRegistryResponse)
end
end

context "when the error message contains package error" do
let(:error_message) do
"ERR_PNPM_NO_VERSIONS  No versions available for prosemirror-gapcursor. The package may be unpublished."
end

it "raises a DependencyFileNotResolvable error with the correct message" do
expect do
error_handler.handle_pnpm_error(error)
end.to raise_error(Dependabot::DependencyFileNotResolvable)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@segment/analytics.js-integration-facebook-pixel": "github:segmentio/analytics.js-integrations#2.4.1"
}
}
Loading
Loading