Skip to content

Generate outdated proxy error #645

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

Open
wants to merge 2 commits into
base: FME-3367
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions Split.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,8 @@
C53EDFAC2DD29640000DCDBC /* TargetingRulesChangeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53EDFAA2DD29640000DCDBC /* TargetingRulesChangeDecoder.swift */; };
C53EDFAE2DD299CB000DCDBC /* RestClientSplitChangesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53EDFAD2DD299CB000DCDBC /* RestClientSplitChangesTest.swift */; };
C53EDFB02DD38572000DCDBC /* RestClientCustomDecoderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53EDFAF2DD38572000DCDBC /* RestClientCustomDecoderTest.swift */; };
C53EDFC42DD3B73A000DCDBC /* RestClientCustomFailureHandlerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53EDFC32DD3B73A000DCDBC /* RestClientCustomFailureHandlerTest.swift */; };
C53EDFC62DD3C53F000DCDBC /* SplitChangesErrorHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53EDFC52DD3C53F000DCDBC /* SplitChangesErrorHandlerTests.swift */; };
C53F3C472DCB956900655753 /* SplitsSyncHelperTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53F3C462DCB956900655753 /* SplitsSyncHelperTest.swift */; };
C53F3C4F2DCD112400655753 /* RuleBasedSegmentChangeProcessorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53F3C4E2DCD110700655753 /* RuleBasedSegmentChangeProcessorStub.swift */; };
C58F33732BDAC4AC00D66549 /* split_unsupported_matcher.json in Resources */ = {isa = PBXBuildFile; fileRef = C58F33722BDAC4AC00D66549 /* split_unsupported_matcher.json */; };
Expand Down Expand Up @@ -1965,6 +1967,8 @@
C53EDFAA2DD29640000DCDBC /* TargetingRulesChangeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetingRulesChangeDecoder.swift; sourceTree = "<group>"; };
C53EDFAD2DD299CB000DCDBC /* RestClientSplitChangesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestClientSplitChangesTest.swift; sourceTree = "<group>"; };
C53EDFAF2DD38572000DCDBC /* RestClientCustomDecoderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestClientCustomDecoderTest.swift; sourceTree = "<group>"; };
C53EDFC32DD3B73A000DCDBC /* RestClientCustomFailureHandlerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestClientCustomFailureHandlerTest.swift; sourceTree = "<group>"; };
C53EDFC52DD3C53F000DCDBC /* SplitChangesErrorHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitChangesErrorHandlerTests.swift; sourceTree = "<group>"; };
C53F3C462DCB956900655753 /* SplitsSyncHelperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitsSyncHelperTest.swift; sourceTree = "<group>"; };
C53F3C4E2DCD110700655753 /* RuleBasedSegmentChangeProcessorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleBasedSegmentChangeProcessorStub.swift; sourceTree = "<group>"; };
C58F33722BDAC4AC00D66549 /* split_unsupported_matcher.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = split_unsupported_matcher.json; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2843,6 +2847,8 @@
592C6AA6211B6C99002D120C /* SplitTests */ = {
isa = PBXGroup;
children = (
C53EDFC52DD3C53F000DCDBC /* SplitChangesErrorHandlerTests.swift */,
C53EDFC32DD3B73A000DCDBC /* RestClientCustomFailureHandlerTest.swift */,
C53EDFAF2DD38572000DCDBC /* RestClientCustomDecoderTest.swift */,
C53EDFAD2DD299CB000DCDBC /* RestClientSplitChangesTest.swift */,
C53EDFA82DD295E5000DCDBC /* TargetingRulesChangeDecoderTest.swift */,
Expand Down Expand Up @@ -4437,6 +4443,7 @@
954F9B0F2570499700140B81 /* PersistentSplitsStorageTests.swift in Sources */,
955428E82568176C00331356 /* ImpressionDaoStub.swift in Sources */,
95F0569829B7CDD7009F5A68 /* TestDataHelper.swift in Sources */,
C53EDFC62DD3C53F000DCDBC /* SplitChangesErrorHandlerTests.swift in Sources */,
9519A91727D6B9EE00278AEC /* AttributesStorageStub.swift in Sources */,
95ABF513293ABEC7006ED016 /* EventsStorageStub.swift in Sources */,
952FA1262A2E255600264AB5 /* FeatureFlagsSynchronizerStub.swift in Sources */,
Expand Down Expand Up @@ -4486,6 +4493,7 @@
C5977C282BF2B923003E293A /* GreaterThanOrEqualToSemverMatcherTest.swift in Sources */,
9550334A282F0FF400E5330F /* ImpressionsTrackerTest.swift in Sources */,
C5977C012BF27390003E293A /* SemverTest.swift in Sources */,
C53EDFC42DD3B73A000DCDBC /* RestClientCustomFailureHandlerTest.swift in Sources */,
595AD24D24E324D200A7B750 /* Base64UtilsTest.swift in Sources */,
599EDAF12270970500D7DACB /* SplitEventsManagerMock.swift in Sources */,
955B59632811E8E700D105CD /* SplitClientManagerTest.swift in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions Split/Network/HttpClient/HttpError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum HttpError: Error, Equatable {
case clientRelated(code: Int, internalCode: Int)
case couldNotCreateRequest(message: String)
case unknown(code: Int, message: String)
case outdatedProxyError(code: Int, spec: String)
}

// MARK: Get message
Expand Down Expand Up @@ -49,6 +50,8 @@ extension HttpError {
return "Request Time Out"
case .uriTooLong:
return "Uri too long"
case .outdatedProxyError(let code, let spec):
return "Outdated proxy error with spec version \(spec) (HTTP \(code))"
}
}

Expand Down
42 changes: 38 additions & 4 deletions Split/Network/RestClient/RestClient+SplitChanges.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,37 @@

import Foundation

/// A handler for specific error conditions in Split Changes requests
fileprivate class SplitChangesErrorHandler {

private let serviceEndpoints: ServiceEndpoints

init(serviceEndpoints: ServiceEndpoints) {
self.serviceEndpoints = serviceEndpoints
}

/// Handles HTTP errors for Split Changes requests
/// - Parameters:
/// - statusCode: The HTTP status code
/// - spec: The spec version used in the request
/// - Returns: A specific HttpError if the conditions match, or nil to fall back to default error handling
func handleError(statusCode: Int, spec: String) -> Error? {
if statusCode == HttpCode.badRequest &&
serviceEndpoints.isCustomSdkEndpoint &&
spec == "1.3" {
return HttpError.outdatedProxyError(code: statusCode, spec: spec)
}
// Return nil to fall back to default error handling
return nil
}
}

protocol RestClientSplitChanges: RestClient {
func getSplitChanges(since: Int64,
rbSince: Int64?,
till: Int64?,
headers: HttpHeaders?,
spec: String,
completion: @escaping (DataResult<TargetingRulesChange>) -> Void)
}

Expand All @@ -21,21 +47,29 @@ extension DefaultRestClient: RestClientSplitChanges {
rbSince: Int64?,
till: Int64?,
headers: HttpHeaders?,
spec: String = Spec.flagsSpec,
completion: @escaping (DataResult<TargetingRulesChange>) -> Void) {

let errorHandler = SplitChangesErrorHandler(serviceEndpoints: endpointFactory.serviceEndpoints)

self.execute(
endpoint: endpointFactory.splitChangesEndpoint,
parameters: buildParameters(since: since, rbSince: rbSince, till: till),
parameters: buildParameters(since: since, rbSince: rbSince, till: till, spec: spec),
headers: headers,
customDecoder: TargetingRulesChangeDecoder.decode,
customFailureHandler: { statusCode in
return errorHandler.handleError(statusCode: statusCode, spec: spec)
},
completion: completion)
}

private func buildParameters(since: Int64,
rbSince: Int64?,
till: Int64?) -> HttpParameters {
till: Int64?,
spec: String) -> HttpParameters {
var parameters: [HttpParameter] = []
if !Spec.flagsSpec.isEmpty() {
parameters.append(HttpParameter(key: "s", value: Spec.flagsSpec))
if !spec.isEmpty {
parameters.append(HttpParameter(key: "s", value: spec))
}

parameters.append(HttpParameter(key: "since", value: since))
Expand Down
5 changes: 0 additions & 5 deletions SplitTests/RestClientCustomDecoderTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,6 @@ class RestClientCustomDecoderTest: XCTestCase {
}
}

private struct TestModel: Decodable {
let id: Int
let name: String
}

private struct CustomTestModel: Decodable {
let custom_id: Int
let custom_name: String
Expand Down
1 change: 1 addition & 0 deletions SplitTests/RestClientCustomFailureHandlerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class RestClientCustomFailureHandlerTest: XCTestCase {
XCTAssertNotNil(error)
XCTAssertTrue(customErrorHandled)

// Verify we got our custom error
if let nsError = error as? NSError {
XCTAssertEqual(nsError.domain, "CustomErrorDomain")
XCTAssertEqual(nsError.code, 999)
Expand Down
179 changes: 179 additions & 0 deletions SplitTests/SplitChangesErrorHandlerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//
// SplitChangesErrorHandlerTests.swift
// SplitTests
//
// Created on 13/05/2025.
// Copyright © 2025 Split. All rights reserved.
//

import XCTest
@testable import Split

class SplitChangesErrorHandlerTests: XCTestCase {

private var httpSession: HttpSessionMock!
private var requestManager: HttpRequestManagerMock!
private var httpClient: HttpClient!

override func setUp() {
super.setUp()
httpSession = HttpSessionMock()
requestManager = HttpRequestManagerMock()
httpClient = DefaultHttpClient(session: httpSession, requestManager: requestManager)
}

override func tearDown() {
httpSession = nil
requestManager = nil
httpClient = nil
super.tearDown()
}

func testSplitChangesWithOutdatedProxyError() {
// Setup with overridden SDK endpoint
let customEndpoint = "https://custom-sdk.split.io"
let overriddenServiceEndpoints = ServiceEndpoints.builder().set(sdkEndpoint: customEndpoint).build()
let overriddenFactory = EndpointFactory(serviceEndpoints: overriddenServiceEndpoints, apiKey: "dummy-api-key", splitsQueryString: "")
let clientWithOverriddenEndpoint = DefaultRestClient(httpClient: httpClient, endpointFactory: overriddenFactory)

// Specific spec version for this test
let testSpec = "1.3"

let expectation = XCTestExpectation(description: "API call completes with outdated proxy error")
var result: TargetingRulesChange?
var error: Error?
var outdatedProxyError: HttpError?

// Call getSplitChanges with the test spec
clientWithOverriddenEndpoint.getSplitChanges(since: 1000, rbSince: 500, till: nil, headers: nil, spec: testSpec) { dataResult in
do {
result = try dataResult.unwrap()
expectation.fulfill()
} catch let err {
error = err
if let httpError = err as? HttpError {
outdatedProxyError = httpError
}
expectation.fulfill()
}
}

// Simulate HTTP 400 response
requestManager.append(data: Data(), to: 1)
_ = requestManager.set(responseCode: HttpCode.badRequest, to: 1)

wait(for: [expectation], timeout: 1)

XCTAssertEqual(1, httpSession.dataTaskCallCount)
XCTAssertEqual(1, requestManager.addRequestCallCount)
XCTAssertNil(result)
XCTAssertNotNil(error)
XCTAssertNotNil(outdatedProxyError)

// Verify we got our custom error
if case .outdatedProxyError(let code, let spec)? = outdatedProxyError {
XCTAssertEqual(code, HttpCode.badRequest)
XCTAssertEqual(spec, testSpec)
} else {
XCTFail("Expected outdatedProxyError error but got \(String(describing: outdatedProxyError))")
}
}

func testSplitChangesWithDifferentStatusCode() {
// Setup with overridden SDK endpoint
let customEndpoint = "https://custom-sdk.split.io"
let overriddenServiceEndpoints = ServiceEndpoints.builder().set(sdkEndpoint: customEndpoint).build()
let overriddenFactory = EndpointFactory(serviceEndpoints: overriddenServiceEndpoints, apiKey: "dummy-api-key", splitsQueryString: "")
let clientWithOverriddenEndpoint = DefaultRestClient(httpClient: httpClient, endpointFactory: overriddenFactory)

// Specific spec version for this test
let testSpec = "1.3"

let expectation = XCTestExpectation(description: "API call completes with internal server error")
var result: TargetingRulesChange?
var error: Error?
var httpError: HttpError?

// Call getSplitChanges with the test spec
clientWithOverriddenEndpoint.getSplitChanges(since: 1000, rbSince: 500, till: nil, headers: nil, spec: testSpec) { dataResult in
do {
result = try dataResult.unwrap()
expectation.fulfill()
} catch let err {
error = err
if let err = err as? HttpError {
httpError = err
}
expectation.fulfill()
}
}

// Simulate HTTP 500 response (not 400)
requestManager.append(data: Data(), to: 1)
_ = requestManager.set(responseCode: HttpCode.internalServerError, to: 1)

wait(for: [expectation], timeout: 1)

XCTAssertEqual(1, httpSession.dataTaskCallCount)
XCTAssertEqual(1, requestManager.addRequestCallCount)
XCTAssertNil(result)
XCTAssertNotNil(error)
XCTAssertNotNil(httpError)

// Verify we got the default error for HTTP 500
if case .unknown(let code, _)? = httpError {
XCTAssertEqual(code, HttpCode.internalServerError)
} else {
XCTFail("Expected HttpError.unknown but got \(String(describing: httpError))")
}
}

func testSplitChangesWithDifferentSpec() {
// Setup with overridden SDK endpoint
let customEndpoint = "https://custom-sdk.split.io"
let overriddenServiceEndpoints = ServiceEndpoints.builder().set(sdkEndpoint: customEndpoint).build()
let overriddenFactory = EndpointFactory(serviceEndpoints: overriddenServiceEndpoints, apiKey: "dummy-api-key", splitsQueryString: "")
let clientWithOverriddenEndpoint = DefaultRestClient(httpClient: httpClient, endpointFactory: overriddenFactory)

// Different spec version (not 1.3)
let testSpec = "1.2q"

let expectation = XCTestExpectation(description: "API call completes with client related error")
var result: TargetingRulesChange?
var error: Error?
var httpError: HttpError?

// Call getSplitChanges with a different spec
clientWithOverriddenEndpoint.getSplitChanges(since: 1000, rbSince: 500, till: nil, headers: nil, spec: testSpec) { dataResult in
do {
result = try dataResult.unwrap()
expectation.fulfill()
} catch let err {
error = err
if let err = err as? HttpError {
httpError = err
}
expectation.fulfill()
}
}

// Simulate HTTP 400 response
requestManager.append(data: Data(), to: 1)
_ = requestManager.set(responseCode: HttpCode.badRequest, to: 1)

wait(for: [expectation], timeout: 1)

XCTAssertEqual(1, httpSession.dataTaskCallCount)
XCTAssertEqual(1, requestManager.addRequestCallCount)
XCTAssertNil(result)
XCTAssertNotNil(error)
XCTAssertNotNil(httpError)

// Verify we got the default client related error (not outdatedProxyError)
if case .clientRelated(let code, _)? = httpError {
XCTAssertEqual(code, HttpCode.badRequest)
} else {
XCTFail("Expected HttpError.clientRelated but got \(String(describing: httpError))")
}
}
}