Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions NGitLab/GitLabRequestResult.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using System;
using System.Net;
using System.Net.Http;

namespace NGitLab;

public sealed class GitLabRequestResult

Check failure on line 6 in NGitLab/GitLabRequestResult.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Symbol 'implicit constructor for 'GitLabRequestResult'' is not part of the declared public API (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 6 in NGitLab/GitLabRequestResult.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Symbol 'GitLabRequestResult' is not part of the declared public API (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)
{
public HttpWebRequest Request { get; set; }
public HttpRequestMessage Request { get; set; }

Check failure on line 8 in NGitLab/GitLabRequestResult.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Check failure on line 8 in NGitLab/GitLabRequestResult.cs

View workflow job for this annotation

GitHub Actions / create_nuget


public WebResponse Response { get; set; }
public HttpResponseMessage Response { get; set; }

Check failure on line 10 in NGitLab/GitLabRequestResult.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Check failure on line 10 in NGitLab/GitLabRequestResult.cs

View workflow job for this annotation

GitHub Actions / create_nuget


public Exception Exception { get; set; }

Check failure on line 12 in NGitLab/GitLabRequestResult.cs

View workflow job for this annotation

GitHub Actions / create_nuget

}
57 changes: 28 additions & 29 deletions NGitLab/Impl/HttpRequestor.GitLabRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,33 +76,33 @@
}
}

public WebResponse GetResponse(RequestOptions options)
public HttpResponseMessage GetResponse(RequestOptions options)
{
Func<WebResponse> getResponseImpl = () => GetResponseImpl(options);
Func<HttpResponseMessage> getResponseImpl = () => GetResponseImpl(options);

return getResponseImpl.Retry(options.ShouldRetry,
options.RetryInterval,
options.RetryCount,
options.IsIncremental);
}

public Task<WebResponse> GetResponseAsync(RequestOptions options, CancellationToken cancellationToken)
public Task<HttpResponseMessage> GetResponseAsync(RequestOptions options, CancellationToken cancellationToken)
{
Func<Task<WebResponse>> getResponseImpl = () => GetResponseImplAsync(options, cancellationToken);
Func<Task<HttpResponseMessage>> getResponseImpl = () => GetResponseImplAsync(options, cancellationToken);

return getResponseImpl.RetryAsync(options.ShouldRetry,
options.RetryInterval,
options.RetryCount,
options.IsIncremental);
}

private WebResponse GetResponseImpl(RequestOptions options)
private HttpResponseMessage GetResponseImpl(RequestOptions options)
{
var result = new GitLabRequestResult();
try
{
result.Request = CreateRequest(options);
result.Response = result.Request.GetResponse();
result.Response = _httpClient.SendAsync(result.Request).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Expand All @@ -116,13 +116,13 @@
return result.Exception is not null ? throw result.Exception : result.Response;
}

private async Task<WebResponse> GetResponseImplAsync(RequestOptions options, CancellationToken cancellationToken)
private async Task<HttpResponseMessage> GetResponseImplAsync(RequestOptions options, CancellationToken cancellationToken)
{
var result = new GitLabRequestResult();
try
{
result.Request = CreateRequest(options);
result.Response = await result.Request.GetResponseAsync().ConfigureAwait(false);
result.Response = await _httpClient.SendAsync(result.Request, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -165,20 +165,22 @@
};
}

private HttpWebRequest CreateRequest(RequestOptions options)
private static HttpClient _httpClient = null;

private HttpRequestMessage CreateRequest(RequestOptions options)
{
var request = WebRequest.CreateHttp(Url);
request.Method = Method.ToString().ToUpperInvariant();
request.Accept = "application/json";
request.Headers = Headers;
request.AutomaticDecompression = DecompressionMethods.GZip;
request.Timeout = (int)options.HttpClientTimeout.TotalMilliseconds;
request.ReadWriteTimeout = (int)options.HttpClientTimeout.TotalMilliseconds;
if (options.Proxy != null)
if (_httpClient != null)
{
request.Proxy = options.Proxy;
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip,
Proxy = options.Proxy,
UseProxy = options.Proxy != null,
};
_httpClient = new HttpClient(handler);
_httpClient.Timeout = options.HttpClientTimeout;
}

Check failure on line 182 in NGitLab/Impl/HttpRequestor.GitLabRequest.cs

View workflow job for this annotation

GitHub Actions / create_nuget


var request = new HttpRequestMessage(new HttpMethod(Method.ToString().ToUpperInvariant()), Url);
if (HasOutput)
{
if (FormData != null)
Expand All @@ -196,29 +198,27 @@
}
else if (Method == MethodType.Put)
{
request.ContentLength = 0;
//request.ContentLength = 0;

Check failure on line 201 in NGitLab/Impl/HttpRequestor.GitLabRequest.cs

View workflow job for this annotation

GitHub Actions / create_nuget

}

return request;
}

private void AddJsonData(HttpWebRequest request, RequestOptions options)
private void AddJsonData(HttpRequestMessage request, RequestOptions options)
{
request.ContentType = "application/json";

request.Content.Headers.ContentType.MediaType = "application/json";
using var writer = new StreamWriter(options.GetRequestStream(request));
writer.Write(JsonData);
writer.Flush();
writer.Close();
}

public void AddFileData(HttpWebRequest request, RequestOptions options)
public void AddFileData(HttpRequestMessage request, RequestOptions options)
{
var boundary = $"--------------------------{DateTime.UtcNow.Ticks.ToStringInvariant()}";
if (Data is not FormDataContent formData)
return;
request.ContentType = "multipart/form-data; boundary=" + boundary;

request.Content.Headers.ContentType.MediaType = "multipart/form-data; boundary=" + boundary;
using var uploadContent = new MultipartFormDataContent(boundary)
{
{ new StreamContent(formData.Stream), "file", formData.Name },
Expand All @@ -227,10 +227,9 @@
uploadContent.CopyToAsync(options.GetRequestStream(request)).Wait();
}

public void AddUrlEncodedData(HttpWebRequest request, RequestOptions options)
public void AddUrlEncodedData(HttpRequestMessage request, RequestOptions options)
{
request.ContentType = "application/x-www-form-urlencoded";

request.Content.Headers.ContentType.MediaType = "application/x-www-form-urlencoded";
using var content = new FormUrlEncodedContent(UrlEncodedData.Values);
content.CopyToAsync(options.GetRequestStream(request)).Wait();
Comment on lines +179 to 181
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

request.Content is null at this point, so accessing request.Content.Headers will throw a NullReferenceException. The content should be created and assigned first:

public void AddUrlEncodedData(HttpRequestMessage request, RequestOptions options)
{
    var content = new FormUrlEncodedContent(UrlEncodedData.Values);
    request.Content = content;
}

This also eliminates the need for GetRequestStream and manual copying.

Suggested change
request.Content.Headers.ContentType.MediaType = "application/x-www-form-urlencoded";
using var content = new FormUrlEncodedContent(UrlEncodedData.Values);
content.CopyToAsync(options.GetRequestStream(request)).Wait();
request.Content = new FormUrlEncodedContent(UrlEncodedData.Values);

Copilot uses AI. Check for mistakes.
}
Expand Down
17 changes: 7 additions & 10 deletions NGitLab/Impl/HttpRequestor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using NGitLab.Impl.Json;
Expand Down Expand Up @@ -144,7 +145,7 @@
using var response = request.GetResponse(_options);
if (parser != null)
{
using var stream = response.GetResponseStream();
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
parser(stream, new WebHeadersDictionaryAdaptor(response.Headers));
}
}
Expand All @@ -159,7 +160,7 @@
using var response = await request.GetResponseAsync(_options, cancellationToken).ConfigureAwait(false);
if (parser != null)
{
using var stream = response.GetResponseStream();
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
await parser(stream, new WebHeadersDictionaryAdaptor(response.Headers)).ConfigureAwait(false);
}
}
Expand Down Expand Up @@ -213,8 +214,7 @@
using (var response = await request.GetResponseAsync(_options, cancellationToken).ConfigureAwait(false))
{
nextUrlToLoad = GetNextPageUrl(response);

using var stream = response.GetResponseStream();
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Using GetAwaiter().GetResult() in an async method defeats the purpose of async/await and can cause deadlocks. Since this is already in an async method, use await instead:

using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
Suggested change
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

Copilot uses AI. Check for mistakes.
responseText = await ReadTextAsync(stream).ConfigureAwait(false);
}

Expand All @@ -234,8 +234,7 @@
using (var response = request.GetResponse(_options))
{
nextUrlToLoad = GetNextPageUrl(response);

using var stream = response.GetResponseStream();
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Using GetAwaiter().GetResult() in synchronous code can cause deadlocks in some contexts. For .NET 8.0, consider using the synchronous ReadAsStream() method with conditional compilation:

#if NET472 || NETSTANDARD2_0
using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
#else
using var stream = response.Content.ReadAsStream();
#endif

This matches the pattern already used in RequestOptions.GetRequestStream.

Copilot uses AI. Check for mistakes.
responseText = ReadText(stream);
}

Expand All @@ -245,19 +244,17 @@
}
}

private static Uri GetNextPageUrl(WebResponse response)
private static Uri GetNextPageUrl(HttpResponseMessage response)
{
// <http://localhost:1080/api/v3/projects?page=2&per_page=0>; rel="next", <http://localhost:1080/api/v3/projects?page=1&per_page=0>; rel="first", <http://localhost:1080/api/v3/projects?page=2&per_page=0>; rel="last"
var link = response.Headers["Link"] ?? response.Headers["Links"];

var link = response.Headers.FirstOrDefault(f => f.Key == "Link").Value.FirstOrDefault();
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The FirstOrDefault() call on line 250 can return null if the "Link" header doesn't exist, but the next line tries to call FirstOrDefault() on it, which will throw a NullReferenceException. The code should handle the null case:

private static Uri GetNextPageUrl(HttpResponseMessage response)
{
    // <http://localhost:1080/api/v3/projects?page=2&per_page=0>; rel="next", <http://localhost:1080/api/v3/projects?page=1&per_page=0>; rel="first", <http://localhost:1080/api/v3/projects?page=2&per_page=0>; rel="last"
    var linkHeader = response.Headers.FirstOrDefault(f => f.Key == "Link");
    var link = linkHeader.Value?.FirstOrDefault();
    
    string[] nextLink = null;
    if (!string.IsNullOrEmpty(link))
    {
        nextLink = link.Split(',')
           .Select(l => l.Split(';'))
           .FirstOrDefault(pair => pair[1].Contains("next"));
    }
    return nextLink != null ? new Uri(nextLink[0].Trim('<', '>', ' ')) : null;
}

Copilot uses AI. Check for mistakes.
string[] nextLink = null;
if (!string.IsNullOrEmpty(link))
{
nextLink = link.Split(',')
.Select(l => l.Split(';'))
.FirstOrDefault(pair => pair[1].Contains("next"));
}

Check failure on line 257 in NGitLab/Impl/HttpRequestor.cs

View workflow job for this annotation

GitHub Actions / create_nuget


return nextLink != null ? new Uri(nextLink[0].Trim('<', '>', ' ')) : null;
}
}
Expand Down
14 changes: 14 additions & 0 deletions NGitLab/Impl/WebHeadersDictionaryAdaptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;

namespace NGitLab.Impl;

Expand All @@ -20,6 +21,19 @@ public WebHeadersDictionaryAdaptor(WebHeaderCollection headers)
_headers = headers ?? throw new ArgumentNullException(nameof(headers));
}

public WebHeadersDictionaryAdaptor(HttpResponseHeaders _headers)
{
var _tmp = _headers ?? throw new ArgumentNullException(nameof(_headers));
_headers.Clear();
_tmp.ToList().ForEach(header =>
{
foreach (var value in header.Value)
{
this._headers.Add(header.Key, value);
}
});
}

public int Count => _headers.Count;

public IEnumerable<string> this[string key] => _headers.GetValues(key) ?? throw new InvalidOperationException("Header not found");
Expand Down
2 changes: 1 addition & 1 deletion NGitLab/NGitLab.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;netstandard2.0;net8.0</TargetFrameworks>
<!-- Keep "WebRequest is obsolete" as a warning -->
Expand Down
4 changes: 2 additions & 2 deletions NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5163,6 +5163,6 @@ virtual NGitLab.Impl.HttpRequestor.StreamAndHeadersAsync(string tailAPIUrl, Syst
virtual NGitLab.Impl.HttpRequestor.StreamAsync(string tailAPIUrl, System.Func<System.IO.Stream, System.Threading.Tasks.Task> parser, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task
virtual NGitLab.Impl.HttpRequestor.To<T>(string tailAPIUrl) -> T
virtual NGitLab.Impl.HttpRequestor.ToAsync<T>(string tailAPIUrl, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<T>
virtual NGitLab.RequestOptions.GetResponse(System.Net.HttpWebRequest request) -> System.Net.WebResponse
virtual NGitLab.RequestOptions.GetResponseAsync(System.Net.HttpWebRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<System.Net.WebResponse>
virtual NGitLab.RequestOptions.GetResponse(System.Net.Http.HttpRequestMessage request) -> System.Net.Http.HttpResponseMessage
virtual NGitLab.RequestOptions.GetResponseAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>
virtual NGitLab.RequestOptions.ShouldRetry(System.Exception ex, int retryNumber) -> bool
10 changes: 8 additions & 2 deletions NGitLab/RequestOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using NGitLab.Impl;

namespace NGitLab;
Expand Down Expand Up @@ -77,8 +78,13 @@ public virtual void ProcessGitLabRequestResult(GitLabRequestResult e)
{
}

internal virtual Stream GetRequestStream(HttpWebRequest request)
internal virtual Stream GetRequestStream(HttpRequestMessage request)
{
return request.GetRequestStream();
#if NET472 || NETSTANDARD2_0
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Extra space in the conditional compilation directive between NET472 and ||.

Suggested change
#if NET472 || NETSTANDARD2_0
#if NET472 || NETSTANDARD2_0

Copilot uses AI. Check for mistakes.
return request.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
#else
return request.Content.ReadAsStream();
#endif

}
}
Loading