From 18a9bc4043c3124e8330dd90d83a8916f8e9748c Mon Sep 17 00:00:00 2001 From: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:49:15 +0000 Subject: [PATCH] Base64-encode `.` and `..` in Pushgateway job and grouping values A job name or grouping key value of `.` or `..` was put into the URL path as-is. Those are dot segments, so the Pushgateway answers with a 307 redirect to the cleaned path and the push fails with HttpRedirectError (proxies may also rewrite the path to a different group). The Pushgateway docs ask for base64 encoding in this case, the same way client_golang does it. Encode these values with `@base64`, like values containing `/`. Signed-off-by: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com> --- lib/prometheus/client/push.rb | 16 ++++++++++++---- spec/prometheus/client/push_spec.rb | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/prometheus/client/push.rb b/lib/prometheus/client/push.rb index ca556ed1..ef250df2 100644 --- a/lib/prometheus/client/push.rb +++ b/lib/prometheus/client/push.rb @@ -89,9 +89,9 @@ def parse(url) def build_path(job, grouping_key) job = job.to_s - # Job can't be empty, but it can contain `/`, so we need to base64 - # encode it in that case - if job.include?('/') + # Job can't be empty, but it can contain `/` or be a dot segment (`.` + # or `..`), so we need to base64 encode it in that case + if needs_base64?(job) encoded_job = Base64.urlsafe_encode64(job) path = "#{PATH}/job@base64/#{encoded_job}" else @@ -101,7 +101,7 @@ def build_path(job, grouping_key) grouping_key.each do |label, value| value = value.to_s - if value.include?('/') + if needs_base64?(value) encoded_value = Base64.urlsafe_encode64(value) path += "/#{label}@base64/#{encoded_value}" # While it's valid for the urlsafe_encode64 function to return an @@ -126,6 +126,14 @@ def build_path(job, grouping_key) path end + # A `/` would split the value into several path segments, and `.` or + # `..` are dot segments that HTTP libraries, proxies and the + # Pushgateway itself normalise away (so the request would end up at a + # different path). The Pushgateway docs ask for base64 in both cases. + def needs_base64?(value) + value.include?('/') || value == '.' || value == '..' + end + def request(req_class, registry = nil) validate_no_label_clashes!(registry) if registry diff --git a/spec/prometheus/client/push_spec.rb b/spec/prometheus/client/push_spec.rb index 2f0b70a5..78af05b3 100644 --- a/spec/prometheus/client/push_spec.rb +++ b/spec/prometheus/client/push_spec.rb @@ -127,6 +127,22 @@ expect(push.path).to eql('/metrics/job/test-job/foo@base64/YmFyL2Jheg==') end + it 'encodes the job name in url-safe base64 if it is a dot segment' do + expect(Prometheus::Client::Push.new(job: '.').path) + .to eql('/metrics/job@base64/Lg==') + expect(Prometheus::Client::Push.new(job: '..').path) + .to eql('/metrics/job@base64/Li4=') + end + + it 'encodes grouping key label values that are dot segments in url-safe base64' do + push = Prometheus::Client::Push.new( + job: 'test-job', + grouping_key: { foo: ".", bar: "..", baz: "..." }, + ) + + expect(push.path).to eql('/metrics/job/test-job/foo@base64/Lg==/bar@base64/Li4=/baz/...') + end + it 'encodes empty grouping key label values as a single base64 padding character' do push = Prometheus::Client::Push.new( job: 'test-job',