Полноценный C# клиент для production
2026-03-10
Полноценная реализация C# клиента для API noncaptcha.com, готовая к использованию в продакшене: обработка ошибок, поддержка отмены операций и все типы капч.
Почему стоит использовать этот клиент?
Вместо ручного написания HTTP-запросов каждый раз, этот переиспользуемый клиент предоставляет:
- Типобезопасность: Строго типизированные запросы и ответы
- Обработка ошибок: Корректные типы исключений со статус-кодами и сырыми телами ответов
- Поддержка отмены: Полная поддержка
CancellationToken - Работа с multipart: Корректные заголовки для загрузки изображений
- Управление ресурсами: Реализует
IDisposableдля корректного освобождения HttpClient
Полный исходный код
Скопируйте и вставьте этот класс напрямую в ваш проект:
C# Client Implementation
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace NonCaptcha
{
public class ApiClient : IDisposable
{
private readonly HttpClient _http;
private readonly string _apiKey;
private readonly JsonSerializerOptions _json;
public ApiClient(string apiKey, string baseUrl = "http://api.noncaptcha.com")
{
if (string.IsNullOrWhiteSpace(baseUrl))
throw new ArgumentException("baseUrl required", nameof(baseUrl));
if (string.IsNullOrWhiteSpace(apiKey))
throw new ArgumentException("apiKey required", nameof(apiKey));
_apiKey = apiKey;
_http = new HttpClient();
_http.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
_http.Timeout = TimeSpan.FromSeconds(10);
_json = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
}
public TimeSpan Timeout
{
get => _http.Timeout;
set => _http.Timeout = value;
}
public void Dispose() => _http.Dispose();
public Task<BalanceResponse> GetBalanceAsync(CancellationToken ct = default)
=> SendJsonAsync<BalanceResponse>(HttpMethod.Get, "api/json/balance", content: null, ct);
public Task<EndpointsResponse> GetEndpointsAsync(CancellationToken ct = default)
=> SendJsonAsync<EndpointsResponse>(HttpMethod.Get, "api/json/endpoints", content: null, ct);
public async Task<TextCaptchaResponse> SolveTextAsync(string fileName, CancellationToken ct = default)
{
var mp = new MultipartFormDataContent();
var imageBytes = await File.ReadAllBytesAsync(fileName, ct);
var imageContent = new ByteArrayContent(imageBytes);
imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
mp.Add(imageContent, "image", Path.GetFileName(fileName));
return await SendJsonAsync<TextCaptchaResponse>(
HttpMethod.Post, "api/json/text", mp, ct
);
}
public async Task<PuzzleCaptchaResponse> SolvePuzzleAsync(
string fileName, string task, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(task))
throw new ArgumentException("task required", nameof(task));
if (string.IsNullOrWhiteSpace(fileName))
throw new ArgumentException("fileName required", nameof(fileName));
var mp = new MultipartFormDataContent();
var imageBytes = await File.ReadAllBytesAsync(fileName, ct);
var imageContent = new ByteArrayContent(imageBytes);
imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
mp.Add(imageContent, "captcha", Path.GetFileName(fileName));
mp.Add(new StringContent(task, Encoding.UTF8), "task");
return await SendJsonAsync<PuzzleCaptchaResponse>(
HttpMethod.Post, "api/json/puzzle", mp, ct
);
}
public async Task<CoordsCaptchaResponse> SolveCoordsAsync(
string captchaFileName, string hintFileName, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(captchaFileName))
throw new ArgumentException("captchaFileName required", nameof(captchaFileName));
if (string.IsNullOrWhiteSpace(hintFileName))
throw new ArgumentException("hintFileName required", nameof(hintFileName));
var mp = new MultipartFormDataContent();
var captchaBytes = await File.ReadAllBytesAsync(captchaFileName, ct);
var captchaContent = new ByteArrayContent(captchaBytes);
captchaContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
mp.Add(captchaContent, "captcha", Path.GetFileName(captchaFileName));
var hintBytes = await File.ReadAllBytesAsync(hintFileName, ct);
var hintContent = new ByteArrayContent(hintBytes);
hintContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
mp.Add(hintContent, "hint", Path.GetFileName(hintFileName));
return await SendJsonAsync<CoordsCaptchaResponse>(
HttpMethod.Post, "api/json/coords", mp, ct
);
}
private async Task<T> SendJsonAsync<T>(
HttpMethod method, string relativeUrl, HttpContent? content, CancellationToken ct)
{
using var req = new HttpRequestMessage(method, relativeUrl);
req.Headers.TryAddWithoutValidation("X-API-Key", _apiKey);
if (content != null)
req.Content = content;
using var resp = await _http.SendAsync(
req, HttpCompletionOption.ResponseHeadersRead, ct
).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (resp.IsSuccessStatusCode)
{
var ok = JsonSerializer.Deserialize<T>(body, _json);
if (ok == null)
throw new NonCaptchaApiException(
resp.StatusCode,
new ErrorResponse("INVALID_RESPONSE", "Empty or invalid JSON response"),
body
);
return ok;
}
ErrorResponse? err = null;
try { err = JsonSerializer.Deserialize<ErrorResponse>(body, _json); } catch { }
err ??= new ErrorResponse("HTTP_ERROR", $"HTTP {(int)resp.StatusCode} {resp.ReasonPhrase}");
throw new NonCaptchaApiException(resp.StatusCode, err, body);
}
}
public sealed record BalanceResponse(double balance);
public sealed record EndpointInfo(string url);
public sealed record EndpointsResponse(List<EndpointInfo> endpoints);
public sealed record TextCaptchaResponse(string text);
public sealed record PuzzleCaptchaResponse(int step);
public sealed record CoordsCaptchaResponse(List<CoordPoint> coords);
public sealed record CoordPoint(int x, int y);
public sealed record ErrorResponse(string error, string message);
public sealed class NonCaptchaApiException : Exception
{
public HttpStatusCode StatusCode { get; }
public ErrorResponse Error { get; }
public string? RawBody { get; }
public NonCaptchaApiException(
HttpStatusCode statusCode, ErrorResponse error, string? rawBody = null)
: base($"{(int)statusCode} {statusCode}: {error.error} - {error.message}")
{
StatusCode = statusCode;
Error = error;
RawBody = rawBody;
}
}
}
Пример использования
Как интегрировать клиент в ваше приложение:
C# Usage
// Initialize once (e.g., in DI container or as singleton)
var client = new ApiClient("YOUR_API_KEY_HERE");
try
{
// Get current balance
var balance = await client.GetBalanceAsync();
Console.WriteLine($"Balance: {balance.balance} RUB");
// Solve text captcha
var textResult = await client.SolveTextAsync("captcha.png");
Console.WriteLine($"Solved text: {textResult.text}");
// Solve puzzle captcha
var puzzleResult = await client.SolvePuzzleAsync(
"puzzle.png",
"[6,8,2,7,0,6,8,0,3,2,8,3,3,0,1,5,2,7,8,6,6,2,6,1,2,6]"
);
Console.WriteLine($"Puzzle step: {puzzleResult.step}");
// Solve coords captcha
var coordsResult = await client.SolveCoordsAsync("main.png", "hint.png");
foreach (var point in coordsResult.coords)
{
Console.WriteLine($"Click at: ({point.x}, {point.y})");
}
}
catch (NonCaptchaApiException ex) when (ex.StatusCode == HttpStatusCode.PaymentRequired)
{
Console.WriteLine("Insufficient balance!");
}
catch (NonCaptchaApiException ex)
{
Console.WriteLine($"API error: {ex.Error.error} - {ex.Error.message}");
Console.WriteLine($"Raw response: {ex.RawBody}");
}
finally
{
// Always dispose to release HttpClient resources
client.Dispose();
}
Рекомендации
- Повторное использование клиента: Создайте один экземпляр и повторно используйте его в течение всего жизненного цикла приложения (HttpClient предназначен для повторного использования).
- Токены отмены: Всегда передавайте токены отмены из слоя приложения для поддержки корректного завершения работы.
-
Обработка ошибок:
Перехватывайте
NonCaptchaApiExceptionотдельно для обработки ошибок API, отличных от сетевых проблем. -
Настройка таймаутов:
Настройте
client.Timeoutв зависимости от типа капчи (пазлы/координаты могут решаться дольше текстовых).